Code import - branch feature/SIENTIAPDE-1646
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/orchestrator/__init__.py
Normal file
0
tests/orchestrator/__init__.py
Normal file
0
tests/orchestrator/activities/__init__.py
Normal file
0
tests/orchestrator/activities/__init__.py
Normal file
148
tests/orchestrator/activities/test_activities.py
Normal file
148
tests/orchestrator/activities/test_activities.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.activities.formatters import Formatters
|
||||
from orchestrator.activities.mongo_db import MongoDB
|
||||
from orchestrator.activities.slot_manager import SlotManager
|
||||
from orchestrator.activities.temporal_manager import TemporalManager
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.MongoDB.__init__')
|
||||
@patch('orchestrator.activities.temporal_manager.TemporalManager.__init__')
|
||||
@patch('orchestrator.activities.slot_manager.SlotManager.__init__')
|
||||
@patch('orchestrator.activities.formatters.Formatters.__init__')
|
||||
@patch('orchestrator.activities.email.Email.__init__')
|
||||
@patch('sientia_do.temporal.activities.postgres_sync.Postgres.__init__')
|
||||
@patch('orchestrator.activities.activities.MetricsController')
|
||||
def test___init__(
|
||||
mock_metrics_controller,
|
||||
mock_postgres_init,
|
||||
mock_email_init,
|
||||
mock_formatters_init,
|
||||
mock_slot_manager_init,
|
||||
mock_temporal_manager_init,
|
||||
mock_mongodb_init,
|
||||
):
|
||||
mongo_db_config = {
|
||||
'connection_string': 'mongodb://localhost:27017',
|
||||
'database_name': 'test_db',
|
||||
'ttl_index_seconds': 3600,
|
||||
}
|
||||
|
||||
redis_config = {'host': 'localhost', 'port': 6379, 'username': 'admin', 'password': 'password'}
|
||||
|
||||
temporal_config = {
|
||||
'temporal_host': 'localhost',
|
||||
'temporal_scouter_namespace': 'scouter',
|
||||
'temporal_laborious_namespace': 'laborious',
|
||||
}
|
||||
|
||||
email_config = {
|
||||
'sender_email': 'test@test.com',
|
||||
'sender_password': 'test',
|
||||
'smtp_server': 'test',
|
||||
'smtp_port': 587,
|
||||
}
|
||||
|
||||
postgres_config = {
|
||||
'host': 'localhost',
|
||||
'port': 5432,
|
||||
'user': 'admin',
|
||||
'password': 'password',
|
||||
'dbname': 'test_db',
|
||||
'min_connections': 1,
|
||||
'max_connections': 10,
|
||||
}
|
||||
|
||||
logger = MagicMock()
|
||||
notification_handler = MagicMock()
|
||||
|
||||
activities = Activities(
|
||||
temporal_config=temporal_config,
|
||||
redis_config=redis_config,
|
||||
mongodb_config=mongo_db_config,
|
||||
email_config=email_config,
|
||||
postgres_config=postgres_config,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
)
|
||||
|
||||
assert isinstance(activities, Activities)
|
||||
assert isinstance(activities, MongoDB)
|
||||
assert isinstance(activities, TemporalManager)
|
||||
assert isinstance(activities, SlotManager)
|
||||
assert isinstance(activities, Formatters)
|
||||
|
||||
mock_slot_manager_init.assert_called_once_with(
|
||||
ANY,
|
||||
host='localhost',
|
||||
port=6379,
|
||||
username='admin',
|
||||
password='password',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
mock_mongodb_init.assert_called_once_with(
|
||||
ANY,
|
||||
connection_string='mongodb://localhost:27017',
|
||||
database_name='test_db',
|
||||
ttl_index_seconds=3600,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
mock_temporal_manager_init.assert_called_once_with(
|
||||
ANY,
|
||||
host='localhost',
|
||||
scouter_namespace='scouter',
|
||||
laborious_namespace='laborious',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
mock_formatters_init.assert_called_once_with(
|
||||
ANY,
|
||||
scouter_namespace='scouter',
|
||||
laborious_namespace='laborious',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=mock_metrics_controller.return_value,
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.activities.MongoDB')
|
||||
@patch('orchestrator.activities.activities.TemporalManager')
|
||||
@patch('orchestrator.activities.activities.SlotManager')
|
||||
@patch('orchestrator.activities.activities.Formatters')
|
||||
@patch('orchestrator.activities.activities.Email')
|
||||
@patch('orchestrator.activities.activities.Postgres')
|
||||
def test_shutdown(
|
||||
mock_mongodb,
|
||||
mock_temporal_manager,
|
||||
mock_slot_manager,
|
||||
mock_formatters,
|
||||
mock_email,
|
||||
mock_postgres,
|
||||
):
|
||||
activities = Activities(
|
||||
temporal_config=MagicMock(),
|
||||
redis_config=MagicMock(),
|
||||
mongodb_config=MagicMock(),
|
||||
email_config=MagicMock(),
|
||||
postgres_config=MagicMock(),
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
)
|
||||
|
||||
activities.shutdown()
|
||||
|
||||
mock_mongodb.close.assert_called()
|
||||
mock_temporal_manager.close.assert_called()
|
||||
mock_slot_manager.close.assert_called()
|
||||
mock_formatters.close.assert_called()
|
||||
mock_email.close.assert_called()
|
||||
mock_postgres.close.assert_called()
|
||||
326
tests/orchestrator/activities/test_email.py
Normal file
326
tests/orchestrator/activities/test_email.py
Normal file
@@ -0,0 +1,326 @@
|
||||
from smtplib import SMTPServerDisconnected
|
||||
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from orchestrator.activities.email import Email
|
||||
|
||||
|
||||
@fixture
|
||||
@patch('orchestrator.activities.email.EmailBuilder')
|
||||
@patch('orchestrator.activities.email.smtplib')
|
||||
def email(smtplib, email_builder):
|
||||
email = Email(
|
||||
sender_email='test@test.com',
|
||||
sender_password='test',
|
||||
smtp_server='test',
|
||||
smtp_port=587,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
email.send_notification = MagicMock()
|
||||
email.emit_metric = AsyncMock()
|
||||
|
||||
return email
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.EmailBuilder')
|
||||
@patch('orchestrator.activities.email.smtplib')
|
||||
def test___init___with_password(smtplib, email_builder):
|
||||
email = Email(
|
||||
sender_email='test@test.com',
|
||||
sender_password='test',
|
||||
smtp_server='test',
|
||||
smtp_port=587,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
|
||||
assert email.sender_email == 'test@test.com'
|
||||
assert email.sender_password == 'test'
|
||||
assert email.smtp_port == 587
|
||||
|
||||
smtplib.SMTP.assert_called_once_with('test', 587, timeout=20)
|
||||
smtplib.SMTP.return_value.starttls.assert_called_once()
|
||||
smtplib.SMTP.return_value.login.assert_called_once_with('test@test.com', 'test')
|
||||
|
||||
assert email.server == smtplib.SMTP.return_value
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.EmailBuilder')
|
||||
@patch('orchestrator.activities.email.smtplib')
|
||||
def test___init___without_password(smtplib, email_builder):
|
||||
email = Email(
|
||||
sender_email='test@test.com',
|
||||
sender_password=None,
|
||||
smtp_server='test',
|
||||
smtp_port=587,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
|
||||
assert email.sender_email == 'test@test.com'
|
||||
assert email.sender_password is None
|
||||
assert email.smtp_port == 587
|
||||
|
||||
smtplib.SMTP.assert_called_once_with('test', 587, timeout=20)
|
||||
assert email.server == smtplib.SMTP.return_value
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test',
|
||||
'model_name': 'test',
|
||||
'model_id': 'test',
|
||||
'workflow_name': 'test',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.SientiaMonitoring')
|
||||
def test_close(sientia_monitoring_mock, email):
|
||||
email.close()
|
||||
|
||||
email.server.quit.assert_called_once()
|
||||
sientia_monitoring_mock.shutdown.assert_called_once()
|
||||
|
||||
|
||||
def test_build_email_html(email):
|
||||
email.email_builder.build_email = MagicMock(return_value='test')
|
||||
input_data = {
|
||||
**metadata,
|
||||
'receiver_groups': {
|
||||
'group_1': {
|
||||
'notifications': [
|
||||
{'type': 'test', 'subject': 'test', 'body': 'test'},
|
||||
{'type': 'test', 'subject': 'test', 'body': 'test'},
|
||||
]
|
||||
}
|
||||
},
|
||||
'mail_type': 'test',
|
||||
}
|
||||
|
||||
response = email.build_email_html(input_data)
|
||||
|
||||
assert response == {
|
||||
'group_1': {
|
||||
'notifications': [
|
||||
{
|
||||
'type': 'test',
|
||||
'subject': 'test',
|
||||
'body': 'test',
|
||||
},
|
||||
{
|
||||
'type': 'test',
|
||||
'subject': 'test',
|
||||
'body': 'test',
|
||||
},
|
||||
],
|
||||
'html': 'test',
|
||||
}
|
||||
}
|
||||
|
||||
email.email_builder.build_email.assert_called_once_with(
|
||||
input_data['receiver_groups']['group_1']['notifications'], input_data['mail_type']
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.MIMEBase')
|
||||
@patch('orchestrator.activities.email.encoders')
|
||||
def test_handle_attachments_success(encoders, mime_base, email):
|
||||
message = MagicMock()
|
||||
|
||||
attachments = [
|
||||
{'filename': 'file_1', 'attachment_content': 'test_content_1'},
|
||||
{'filename': 'file_2', 'attachment_content': 'test_content_2'},
|
||||
{'filename': 'file_3', 'attachment_content': 'test_content_3'},
|
||||
]
|
||||
|
||||
response = email.handle_attachments(attachments, message)
|
||||
|
||||
assert response == message
|
||||
|
||||
mime_base.assert_called_with('application', 'octet-stream')
|
||||
assert mime_base.return_value.set_payload.call_count == 3
|
||||
|
||||
mime_base.return_value.set_payload.assert_has_calls(
|
||||
[
|
||||
call(b'test_content_1'),
|
||||
call(b'test_content_2'),
|
||||
call(b'test_content_3'),
|
||||
]
|
||||
)
|
||||
|
||||
encoders.encode_base64.assert_called_with(mime_base.return_value)
|
||||
assert encoders.encode_base64.call_count == 3
|
||||
|
||||
mime_base.return_value.add_header.assert_has_calls(
|
||||
[
|
||||
call('Content-Disposition', 'attachment; filename="file_1"'),
|
||||
call('Content-Disposition', 'attachment; filename="file_2"'),
|
||||
call('Content-Disposition', 'attachment; filename="file_3"'),
|
||||
]
|
||||
)
|
||||
|
||||
message.attach.assert_called_with(mime_base.return_value)
|
||||
assert message.attach.call_count == 3
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.MIMEBase')
|
||||
def test_handle_attachments_failure(mime_base, email):
|
||||
message = MagicMock()
|
||||
|
||||
mime_base.side_effect = Exception('test')
|
||||
|
||||
attachments = [{'filename': 'file_1', 'attachment_content': 'test_content_1'}]
|
||||
|
||||
try:
|
||||
email.handle_attachments(attachments, message)
|
||||
except Exception as e:
|
||||
assert str(e) == 'test'
|
||||
|
||||
assert message.attach.call_count == 0
|
||||
|
||||
|
||||
def test_try_send_email_success(email):
|
||||
email.server.sendmail = MagicMock()
|
||||
|
||||
msg = MagicMock()
|
||||
|
||||
email.try_send_email(msg, 'test')
|
||||
|
||||
email.server.sendmail.assert_called_once_with(
|
||||
'test@test.com', 'test', msg.as_string.return_value
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.smtplib.SMTP')
|
||||
def test_try_send_email_reconnect_quit_success(smtp, email):
|
||||
email.server.sendmail = MagicMock(side_effect=SMTPServerDisconnected('test'))
|
||||
email.server.quit = MagicMock()
|
||||
|
||||
msg = MagicMock()
|
||||
|
||||
email.try_send_email(msg, 'test')
|
||||
|
||||
smtp.assert_has_calls([call('test', 587, timeout=20)])
|
||||
|
||||
smtp.return_value.starttls.assert_called_once()
|
||||
smtp.return_value.login.assert_called_once_with('test@test.com', 'test')
|
||||
|
||||
smtp.return_value.sendmail.assert_called_once_with(
|
||||
'test@test.com', 'test', msg.as_string.return_value
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.smtplib.SMTP')
|
||||
def test_try_send_email_reconnect_quit_failure_disconnect(smtp, email):
|
||||
email.server.sendmail = MagicMock(side_effect=SMTPServerDisconnected('test'))
|
||||
email.server.quit = MagicMock(side_effect=SMTPServerDisconnected('test'))
|
||||
|
||||
msg = MagicMock()
|
||||
|
||||
email.try_send_email(msg, 'test')
|
||||
|
||||
smtp.assert_has_calls([call('test', 587, timeout=20)])
|
||||
smtp.return_value.starttls.assert_called_once()
|
||||
smtp.return_value.login.assert_called_once_with('test@test.com', 'test')
|
||||
|
||||
smtp.return_value.sendmail.assert_called_once_with(
|
||||
'test@test.com', 'test', msg.as_string.return_value
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.smtplib.SMTP')
|
||||
def test_try_send_email_reconnect_quit_failure(smtp, email):
|
||||
email.server.sendmail = MagicMock(side_effect=SMTPServerDisconnected('test'))
|
||||
email.server.quit = MagicMock(side_effect=Exception('test'))
|
||||
|
||||
msg = MagicMock()
|
||||
|
||||
try:
|
||||
email.try_send_email(msg, 'test')
|
||||
except Exception as e:
|
||||
assert str(e) == 'test'
|
||||
else:
|
||||
raise AssertionError('Expected exception')
|
||||
|
||||
|
||||
def test_send_email_without_smtp_server(email):
|
||||
email.smtp_server = None
|
||||
input_data = {**metadata, 'receiver_groups': {}, 'mail_type': 'test_TYPE'}
|
||||
response = email.send_email(input_data)
|
||||
|
||||
assert response == {}
|
||||
|
||||
|
||||
@patch('orchestrator.activities.email.MIMEText')
|
||||
@patch('orchestrator.activities.email.MIMEMultipart')
|
||||
def test_send_email(mimemultipart, mimetext, email):
|
||||
side_effect_1 = MagicMock()
|
||||
side_effect_2 = MagicMock()
|
||||
mimemultipart.side_effect = [side_effect_1, side_effect_2]
|
||||
|
||||
email.email_builder.send_notification = MagicMock()
|
||||
|
||||
email.try_send_email = MagicMock(side_effect=[None, Exception('test')])
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'receiver_groups': {
|
||||
'group_1': {
|
||||
'members': ['test@test.com', 'test2@test.com'],
|
||||
'notifications': [
|
||||
{
|
||||
'attachment_content': 'test_content_1',
|
||||
'trigger': 'test_trigger',
|
||||
'notification_id': 'test_notification_id',
|
||||
}
|
||||
],
|
||||
'html': 'test_html1',
|
||||
},
|
||||
'group_2': {
|
||||
'members': ['test3@test.com', 'test4@test.com'],
|
||||
'notifications': [],
|
||||
'html': 'test_html2',
|
||||
},
|
||||
},
|
||||
'mail_type': 'test_TYPE',
|
||||
}
|
||||
|
||||
response = email.send_email(input_data)
|
||||
|
||||
assert response['group_1']['status'] == 'sent'
|
||||
assert response['group_2']['status'] == 'failed'
|
||||
|
||||
assert mimemultipart.call_count == 2
|
||||
|
||||
mimetext.assert_has_calls([call('test_html1', 'html'), call('test_html2', 'html')])
|
||||
|
||||
side_effect_1.__setitem__.assert_has_calls(
|
||||
[
|
||||
call('From', 'test@test.com'),
|
||||
call('To', 'test@test.com, test2@test.com'),
|
||||
call('Subject', 'SIENTIA™ test_TYPE'),
|
||||
]
|
||||
)
|
||||
|
||||
side_effect_2.__setitem__.assert_has_calls(
|
||||
[
|
||||
call('From', 'test@test.com'),
|
||||
call('To', 'test3@test.com, test4@test.com'),
|
||||
call('Subject', 'SIENTIA™ test_TYPE'),
|
||||
]
|
||||
)
|
||||
|
||||
email.try_send_email.assert_has_calls(
|
||||
[
|
||||
call(side_effect_1, 'test@test.com, test2@test.com'),
|
||||
call(side_effect_2, 'test3@test.com, test4@test.com'),
|
||||
]
|
||||
)
|
||||
|
||||
assert email.try_send_email.call_count == 2
|
||||
768
tests/orchestrator/activities/test_formatters.py
Normal file
768
tests/orchestrator/activities/test_formatters.py
Normal file
@@ -0,0 +1,768 @@
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pandas import DataFrame
|
||||
from pytest import fixture
|
||||
from sientia_do.notifications.models import NotificationLevel
|
||||
|
||||
from orchestrator.activities.formatters import Formatters
|
||||
|
||||
|
||||
@fixture
|
||||
def formatters():
|
||||
formatters = Formatters(
|
||||
scouter_namespace='scouter',
|
||||
laborious_namespace='laborious',
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
|
||||
formatters.send_notification = MagicMock()
|
||||
formatters.emit_metric = AsyncMock()
|
||||
formatters.error = MagicMock()
|
||||
formatters.info = MagicMock()
|
||||
formatters.debug = MagicMock()
|
||||
return formatters
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test_schedule_name',
|
||||
'workflow_name': 'test_workflow_name',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_process_schedules(formatters):
|
||||
mock_scouter = MagicMock(return_value={'test_scouter': 'test_scouter'})
|
||||
mock_predictions_batch = MagicMock(
|
||||
return_value={'test_predictions_batch': 'test_predictions_batch'}
|
||||
)
|
||||
mock_minimal_retrain = MagicMock(return_value={'test_minimal_retrain': 'test_minimal_retrain'})
|
||||
mock_drift = MagicMock(return_value={'test_drift': 'test_drift'})
|
||||
mock_simple_metrics = MagicMock(return_value={'test_simple_metrics': 'test_simple_metrics'})
|
||||
|
||||
mock_schedule_types = {
|
||||
'scouter': {
|
||||
'namespace': 'scouter',
|
||||
'function': mock_scouter,
|
||||
},
|
||||
'pi_web_api_scouter': {
|
||||
'namespace': 'scouter',
|
||||
'function': mock_scouter,
|
||||
},
|
||||
'predictions_batch': {
|
||||
'namespace': 'laborious',
|
||||
'function': mock_predictions_batch,
|
||||
},
|
||||
'minimal_retrain': {
|
||||
'namespace': 'laborious',
|
||||
'function': mock_minimal_retrain,
|
||||
},
|
||||
'drift': {
|
||||
'namespace': 'laborious',
|
||||
'function': mock_drift,
|
||||
},
|
||||
'simple_metrics': {
|
||||
'namespace': 'laborious',
|
||||
'function': mock_simple_metrics,
|
||||
},
|
||||
}
|
||||
|
||||
input_data = {
|
||||
'pipelines': [
|
||||
{
|
||||
'schedule_name': 'test_schedule_name',
|
||||
'workflow_type': 'scouter',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-01',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name2',
|
||||
'workflow_type': 'predictions_batch',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-02',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name3',
|
||||
'workflow_type': 'minimal_retrain',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-03',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name4',
|
||||
'workflow_type': 'drift',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-04',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name5',
|
||||
'workflow_type': 'simple_metrics',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-05',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
with patch('orchestrator.activities.formatters.schedule_types', mock_schedule_types):
|
||||
result = formatters.process_schedules(input_data)
|
||||
|
||||
assert result == {
|
||||
'scouter': {
|
||||
'test_schedule_name': {'test_scouter': 'test_scouter', 'updated_at': '2021-01-01'}
|
||||
},
|
||||
'laborious': {
|
||||
'test_schedule_name2': {
|
||||
'test_predictions_batch': 'test_predictions_batch',
|
||||
'updated_at': '2021-01-02',
|
||||
},
|
||||
'test_schedule_name3': {
|
||||
'test_minimal_retrain': 'test_minimal_retrain',
|
||||
'updated_at': '2021-01-03',
|
||||
},
|
||||
'test_schedule_name4': {
|
||||
'test_drift': 'test_drift',
|
||||
'updated_at': '2021-01-04',
|
||||
},
|
||||
'test_schedule_name5': {
|
||||
'test_simple_metrics': 'test_simple_metrics',
|
||||
'updated_at': '2021-01-05',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mock_scouter.assert_called_once_with(input_data['pipelines'][0])
|
||||
mock_predictions_batch.assert_called_once_with(input_data['pipelines'][1])
|
||||
mock_minimal_retrain.assert_called_once_with(input_data['pipelines'][2])
|
||||
mock_drift.assert_called_once_with(input_data['pipelines'][3])
|
||||
mock_simple_metrics.assert_called_once_with(input_data['pipelines'][4])
|
||||
|
||||
|
||||
def test_process_schedules_with_invalid_workflow_type(formatters):
|
||||
"""Test that process_schedules handles invalid workflow types correctly"""
|
||||
mock_scouter = MagicMock(return_value={'test_scouter': 'test_scouter'})
|
||||
|
||||
mock_schedule_types = {
|
||||
'scouter': {
|
||||
'namespace': 'scouter',
|
||||
'function': mock_scouter,
|
||||
},
|
||||
}
|
||||
|
||||
pipelines = [
|
||||
{
|
||||
'schedule_name': 'test_schedule_name_valid',
|
||||
'workflow_type': 'scouter',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-01',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name_invalid',
|
||||
'workflow_type': 'invalid_workflow_type',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-02',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test_schedule_name_valid2',
|
||||
'workflow_type': 'scouter',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'updated_at': '2021-01-03',
|
||||
},
|
||||
]
|
||||
|
||||
input_data = {
|
||||
'pipelines': pipelines,
|
||||
'metadata': {
|
||||
'schedule_name': 'test_schedule',
|
||||
'workflow_name': 'test_workflow',
|
||||
},
|
||||
}
|
||||
|
||||
with patch('orchestrator.activities.formatters.schedule_types', mock_schedule_types):
|
||||
result = formatters.process_schedules(input_data)
|
||||
|
||||
# Assert that error was called for invalid workflow type
|
||||
formatters.error.assert_called_once_with(
|
||||
'Workflow type invalid_workflow_type not supported', metadata=input_data['metadata']
|
||||
)
|
||||
|
||||
# Assert that only valid pipelines were processed
|
||||
assert result == {
|
||||
'scouter': {
|
||||
'test_schedule_name_valid': {
|
||||
'test_scouter': 'test_scouter',
|
||||
'updated_at': '2021-01-01',
|
||||
},
|
||||
'test_schedule_name_valid2': {
|
||||
'test_scouter': 'test_scouter',
|
||||
'updated_at': '2021-01-03',
|
||||
},
|
||||
},
|
||||
'laborious': {},
|
||||
}
|
||||
|
||||
# Assert that the mock function was called only for valid pipelines
|
||||
assert mock_scouter.call_count == 2
|
||||
mock_scouter.assert_any_call(pipelines[0])
|
||||
mock_scouter.assert_any_call(pipelines[2])
|
||||
|
||||
|
||||
@patch(
|
||||
'orchestrator.activities.formatters.gather_read_tags',
|
||||
return_value={
|
||||
'1:test_tag_address': {
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
'topics': ['raw_test_schedule'],
|
||||
'frequency': 1000,
|
||||
},
|
||||
'2:test_tag_address2': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address2',
|
||||
'topics': ['raw_test_schedule2'],
|
||||
'frequency': 1000,
|
||||
},
|
||||
'2:test_tag_address3': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address3',
|
||||
'topics': ['raw_test_schedule2'],
|
||||
'frequency': 1000,
|
||||
},
|
||||
},
|
||||
)
|
||||
@patch('orchestrator.activities.formatters.build_tag_config')
|
||||
def test_process_slots(mock_build_tag_config, mock_gather_read_tags, formatters):
|
||||
input_data = {
|
||||
'opc_servers': [
|
||||
{'id': '1', 'server_name': 'test_server_name', 'url': 'test_url', 'uri': 'test_uri'},
|
||||
{'id': '2', 'server_name': 'test_server_name2', 'url': 'test_url2', 'uri': 'test_uri2'},
|
||||
],
|
||||
'active_ingestors': ['test_active_ingestor1', 'test_active_ingestor2'],
|
||||
'pipelines': 'test_gather_read_tags',
|
||||
**metadata,
|
||||
}
|
||||
|
||||
expected_opc_servers = {
|
||||
'1': {
|
||||
'id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'url': 'test_url',
|
||||
'uri': 'test_uri',
|
||||
},
|
||||
'2': {
|
||||
'id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'url': 'test_url2',
|
||||
'uri': 'test_uri2',
|
||||
},
|
||||
}
|
||||
|
||||
slot_mock = {
|
||||
'test_server_name': {
|
||||
'server_id': '1',
|
||||
'name': 'test_server_name',
|
||||
'url': 'test_url',
|
||||
'server_uri': 'test_uri',
|
||||
'cert_path': None,
|
||||
'private_key_path': None,
|
||||
'server_cert_path': None,
|
||||
}
|
||||
}
|
||||
|
||||
mock_build_tag_config.return_value = (slot_mock, ['2'])
|
||||
|
||||
result = formatters.process_slots(input_data)
|
||||
|
||||
tags = list(mock_gather_read_tags.return_value.values())
|
||||
|
||||
mock_gather_read_tags.assert_called_once_with(input_data['pipelines'])
|
||||
|
||||
mock_build_tag_config.assert_has_calls(
|
||||
[
|
||||
call(tags[:2], expected_opc_servers),
|
||||
call(tags[2:], expected_opc_servers),
|
||||
]
|
||||
)
|
||||
|
||||
formatters.send_notification.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='ORCHESTRATOR_SERVER_NOT_FOUND_DURING_SLOT_CONFIGURATION',
|
||||
message='Servers 2 not found in opc_servers',
|
||||
block='orchestrator',
|
||||
level=NotificationLevel.ERROR,
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='ORCHESTRATOR_SERVER_NOT_FOUND_DURING_SLOT_CONFIGURATION',
|
||||
message='Servers 2 not found in opc_servers',
|
||||
block='orchestrator',
|
||||
level=NotificationLevel.ERROR,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert result == {
|
||||
'1': slot_mock,
|
||||
'2': slot_mock,
|
||||
}
|
||||
|
||||
|
||||
def test_format_schedule_config(formatters):
|
||||
input_data = {
|
||||
'schedule_config': [
|
||||
{'namespace': 'test_namespace1', 'schedule_name': 'test1', 'updated_at': '2021-01-01'},
|
||||
{'namespace': 'test_namespace2', 'schedule_name': 'test2', 'updated_at': '2021-01-02'},
|
||||
],
|
||||
**metadata,
|
||||
}
|
||||
|
||||
result = formatters.format_schedule_config(input_data)
|
||||
|
||||
assert result == {
|
||||
'test_namespace1': {'test1': '2021-01-01'},
|
||||
'test_namespace2': {'test2': '2021-01-02'},
|
||||
}
|
||||
|
||||
|
||||
def test_create_schedule_config(formatters):
|
||||
input_data = {
|
||||
'current_schedule_config': {
|
||||
'scouter': {
|
||||
'test_schedule_name_to_delete': '2021-01-01',
|
||||
'test_schedule_name_to_update': '2021-01-02',
|
||||
}
|
||||
},
|
||||
'schedule_config': {
|
||||
'laborious': {
|
||||
'test_schedule_name_to_create': {
|
||||
'frequency': 60,
|
||||
'data': {'test': 'test'},
|
||||
'updated_at': '2021-01-03',
|
||||
}
|
||||
},
|
||||
'scouter': {
|
||||
'test_schedule_name_to_update': {
|
||||
'frequency': 60,
|
||||
'data': {'test': 'test2'},
|
||||
'updated_at': '2021-01-04',
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result = formatters.create_schedule_config(input_data)
|
||||
|
||||
assert result == {
|
||||
'to_create': {
|
||||
'laborious': {
|
||||
'test_schedule_name_to_create': {
|
||||
'frequency': 60,
|
||||
'data': {'test': 'test'},
|
||||
'updated_at': '2021-01-03',
|
||||
}
|
||||
},
|
||||
'scouter': {},
|
||||
},
|
||||
'to_update': {
|
||||
'scouter': {
|
||||
'test_schedule_name_to_update': {
|
||||
'frequency': 60,
|
||||
'data': {'test': 'test2'},
|
||||
'updated_at': '2021-01-04',
|
||||
}
|
||||
},
|
||||
'laborious': {},
|
||||
},
|
||||
'to_delete': {'scouter': ['test_schedule_name_to_delete'], 'laborious': []},
|
||||
}
|
||||
|
||||
|
||||
def test_create_slot_config(formatters):
|
||||
input_data = {
|
||||
'current_slot_config': {
|
||||
'1': {'frequency': 60, 'data': {'test': 'test'}},
|
||||
'2': {'frequency': 60, 'data': {'test': 'test'}},
|
||||
},
|
||||
'slot_config': {'1': {'frequency': 60, 'data': {'test': 'test2'}}},
|
||||
}
|
||||
|
||||
result = formatters.create_slot_config(input_data)
|
||||
|
||||
assert result == {
|
||||
'to_delete': ['2'],
|
||||
'to_insert': {'1': {'frequency': 60, 'data': {'test': 'test2'}}},
|
||||
}
|
||||
|
||||
|
||||
def test_send_success_report(formatters):
|
||||
formatters.send_success_report(
|
||||
metadata=metadata,
|
||||
message='test_message',
|
||||
notification_id='test_notification_id',
|
||||
attachment={'test': 'test'},
|
||||
)
|
||||
formatters.send_notification.assert_called_once_with(
|
||||
metadata=metadata,
|
||||
notification_id='test_notification_id',
|
||||
message='test_message',
|
||||
block='report_orchestration',
|
||||
level=NotificationLevel.INFO,
|
||||
attachment_content=json.dumps({'test': 'test'}, indent=4, sort_keys=True),
|
||||
)
|
||||
|
||||
|
||||
def test_send_error_report(formatters):
|
||||
formatters.send_error_report(
|
||||
metadata=metadata,
|
||||
message='test_message',
|
||||
notification_id='test_notification_id',
|
||||
attachment='test_attachment',
|
||||
)
|
||||
formatters.send_notification.assert_called_once_with(
|
||||
metadata=metadata,
|
||||
notification_id='test_notification_id',
|
||||
message='test_message',
|
||||
block='report_orchestration',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content='test_attachment',
|
||||
)
|
||||
|
||||
|
||||
def test_parse_report(formatters):
|
||||
input_data = {
|
||||
'test_key': {'success': True},
|
||||
'test_key2': {'success': False, 'message': 'test_error'},
|
||||
}
|
||||
|
||||
result = formatters.parse_report(input_data)
|
||||
|
||||
assert result == (['test_key'], ['test_key2'])
|
||||
|
||||
|
||||
def test_parse_report_schedule(formatters):
|
||||
input_data = [
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_create',
|
||||
'success': True,
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_create_error',
|
||||
'success': False,
|
||||
'message': 'test_error',
|
||||
'attachment': 'test_attachment',
|
||||
},
|
||||
]
|
||||
result = formatters.parse_report_schedule(input_data)
|
||||
|
||||
assert result == (
|
||||
['test_namespace/test_schedule_name_to_create'],
|
||||
{
|
||||
'test_namespace/test_schedule_name_to_create_error': {
|
||||
'message': 'test_error',
|
||||
'attachment': 'test_attachment',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_report_schedule_orchestration(formatters):
|
||||
formatters.parse_report_schedule = MagicMock(side_effect=formatters.parse_report_schedule)
|
||||
formatters.send_success_report = AsyncMock()
|
||||
formatters.send_error_report = AsyncMock()
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'created_schedules': [
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_create',
|
||||
'success': True,
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_create_error',
|
||||
'success': False,
|
||||
'message': 'test_error1',
|
||||
'attachment': 'test_attachment1',
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_create_error2',
|
||||
'success': False,
|
||||
'message': 'test_error2',
|
||||
},
|
||||
],
|
||||
'updated_schedules': [
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_update',
|
||||
'success': True,
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_update_error',
|
||||
'success': False,
|
||||
'message': 'test_error2',
|
||||
'attachment': 'test_attachment2',
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_update_error2',
|
||||
'success': False,
|
||||
'message': 'test_error3',
|
||||
'attachment': 'test_attachment3',
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_update_error3',
|
||||
'success': False,
|
||||
'message': 'test_error4',
|
||||
},
|
||||
],
|
||||
'deleted_schedules': [
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_delete',
|
||||
'success': True,
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_delete_error',
|
||||
'success': False,
|
||||
'message': 'test_error4',
|
||||
'attachment': 'test_attachment4',
|
||||
},
|
||||
{
|
||||
'namespace': 'test_namespace',
|
||||
'schedule_name': 'test_schedule_name_to_delete_error2',
|
||||
'success': False,
|
||||
'message': 'test_error5',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
formatters.report_schedule_orchestration(input_data)
|
||||
|
||||
formatters.parse_report_schedule.assert_has_calls(
|
||||
[
|
||||
call(input_data['created_schedules']),
|
||||
call(input_data['updated_schedules']),
|
||||
call(input_data['deleted_schedules']),
|
||||
]
|
||||
)
|
||||
formatters.send_success_report.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Successfully created schedules: \n test_namespace/test_schedule_name_to_create',
|
||||
notification_id='REPORT_ORCHESTRATION_CREATED_SCHEDULES',
|
||||
attachment=input_data['created_schedules'],
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Successfully updated schedules: \n test_namespace/test_schedule_name_to_update',
|
||||
notification_id='REPORT_ORCHESTRATION_UPDATED_SCHEDULES',
|
||||
attachment=input_data['updated_schedules'],
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Successfully deleted schedules: \n test_namespace/test_schedule_name_to_delete',
|
||||
notification_id='REPORT_ORCHESTRATION_DELETED_SCHEDULES',
|
||||
attachment=input_data['deleted_schedules'],
|
||||
),
|
||||
]
|
||||
)
|
||||
formatters.send_error_report.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Fails on created schedules: \n test_namespace/test_schedule_name_to_create_error, test_namespace/test_schedule_name_to_create_error2',
|
||||
notification_id='REPORT_ORCHESTRATION_CREATED_SCHEDULES_ERROR',
|
||||
attachment='test_namespace/test_schedule_name_to_create_error:\ntest_error1\ntest_attachment1\n ========== \ntest_namespace/test_schedule_name_to_create_error2:\ntest_error2',
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Fails on updated schedules: \n test_namespace/test_schedule_name_to_update_error, test_namespace/test_schedule_name_to_update_error2, test_namespace/test_schedule_name_to_update_error3',
|
||||
notification_id='REPORT_ORCHESTRATION_UPDATED_SCHEDULES_ERROR',
|
||||
attachment='test_namespace/test_schedule_name_to_update_error:\ntest_error2\ntest_attachment2\n ========== \ntest_namespace/test_schedule_name_to_update_error2:\ntest_error3\ntest_attachment3\n ========== \ntest_namespace/test_schedule_name_to_update_error3:\ntest_error4',
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Fails on deleted schedules: \n test_namespace/test_schedule_name_to_delete_error, test_namespace/test_schedule_name_to_delete_error2',
|
||||
notification_id='REPORT_ORCHESTRATION_DELETED_SCHEDULES_ERROR',
|
||||
attachment='test_namespace/test_schedule_name_to_delete_error:\ntest_error4\ntest_attachment4\n ========== \ntest_namespace/test_schedule_name_to_delete_error2:\ntest_error5',
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_report_slot_orchestration(formatters):
|
||||
formatters.parse_report = MagicMock(side_effect=formatters.parse_report)
|
||||
formatters.send_success_report = AsyncMock()
|
||||
formatters.send_error_report = AsyncMock()
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'inserted_slots': {
|
||||
'test_slot_name_to_create': {'success': True},
|
||||
'test_slot_name_to_create_error': {'success': False, 'error': 'test_error'},
|
||||
},
|
||||
'deleted_slots': {
|
||||
'test_slot_name_to_delete': {'success': True},
|
||||
'test_slot_name_to_delete_error': {'success': False, 'error': 'test_error'},
|
||||
},
|
||||
}
|
||||
|
||||
formatters.report_slot_orchestration(input_data)
|
||||
|
||||
formatters.parse_report.assert_has_calls(
|
||||
[call(input_data['inserted_slots']), call(input_data['deleted_slots'])]
|
||||
)
|
||||
formatters.send_success_report.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Inserted slots: \n test_slot_name_to_create',
|
||||
notification_id='REPORT_ORCHESTRATION_INSERTED_SLOTS',
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Deleted slots: \n test_slot_name_to_delete',
|
||||
notification_id='REPORT_ORCHESTRATION_DELETED_SLOTS',
|
||||
),
|
||||
]
|
||||
)
|
||||
formatters.send_error_report.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Failed to insert slots: \n test_slot_name_to_create_error',
|
||||
notification_id='REPORT_ORCHESTRATION_INSERTED_SLOTS',
|
||||
attachment=input_data['inserted_slots'],
|
||||
),
|
||||
call(
|
||||
metadata=metadata['metadata'],
|
||||
message='Failed to delete slots: \n test_slot_name_to_delete_error',
|
||||
notification_id='REPORT_ORCHESTRATION_DELETED_SLOTS',
|
||||
attachment=input_data['deleted_slots'],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_format_log_report(formatters):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'receiver_groups': {
|
||||
'test_receiver_group': {
|
||||
'notifications': [
|
||||
{
|
||||
'notification_id': 'test_notification_id',
|
||||
'trigger': 'test_trigger',
|
||||
'timestamp': '2021-01-01',
|
||||
'message': 'test_message',
|
||||
'level': 'test_level',
|
||||
'block': 'test_block',
|
||||
'pipeline': 'test_pipeline',
|
||||
'project': 'test_project',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
],
|
||||
'status': 'sent',
|
||||
},
|
||||
'test_receiver_group2': {
|
||||
'notifications': [
|
||||
{
|
||||
'notification_id': 'test_notification_id',
|
||||
'trigger': 'test_trigger',
|
||||
'timestamp': '2021-01-01',
|
||||
'message': 'test_message',
|
||||
'level': 'test_level',
|
||||
'block': 'test_block',
|
||||
'pipeline': 'test_pipeline',
|
||||
'project': 'test_project',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
],
|
||||
'status': 'sent',
|
||||
},
|
||||
},
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
result = formatters.format_log_report(input_data)
|
||||
|
||||
expected_result = DataFrame(
|
||||
[
|
||||
{
|
||||
'status': 'sent',
|
||||
'timestamp': '2021-01-01',
|
||||
'groups': ['test_receiver_group', 'test_receiver_group2'],
|
||||
'message': 'test_message',
|
||||
'level': 'test_level',
|
||||
'notification_id': 'test_notification_id',
|
||||
'block': 'test_block',
|
||||
'schedule': 'test_trigger',
|
||||
'pipeline': 'test_pipeline',
|
||||
'project': 'test_project',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert DataFrame(result).equals(expected_result)
|
||||
|
||||
|
||||
def test_filter_notification_reports(formatters):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'notification_package': [
|
||||
{'trigger': 'test_trigger_1', 'notification_id': 'test_notification_id_1'},
|
||||
{'trigger': 'test_trigger_2', 'notification_id': 'test_notification_id_2'},
|
||||
{'trigger': 'test_trigger_3', 'notification_id': 'test_notification_id_3'},
|
||||
],
|
||||
'sending_configs': [
|
||||
{
|
||||
'group_name': 'test_group_1',
|
||||
'contents': ['reports'],
|
||||
'ignore': ['test_notification_id_1'],
|
||||
},
|
||||
{'group_name': 'test_group_2', 'contents': ['core_alerts']},
|
||||
],
|
||||
}
|
||||
|
||||
response = formatters.filter_notification_reports(input_data)
|
||||
|
||||
assert response == {
|
||||
'test_group_1': {
|
||||
'group_name': 'test_group_1',
|
||||
'contents': ['reports'],
|
||||
'ignore': ['test_notification_id_1'],
|
||||
'notifications': [
|
||||
{'trigger': 'test_trigger_2', 'notification_id': 'test_notification_id_2'},
|
||||
{'trigger': 'test_trigger_3', 'notification_id': 'test_notification_id_3'},
|
||||
],
|
||||
}
|
||||
}
|
||||
545
tests/orchestrator/activities/test_mongo_db.py
Normal file
545
tests/orchestrator/activities/test_mongo_db.py
Normal file
@@ -0,0 +1,545 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||
|
||||
from pytest import fixture
|
||||
from sientia_do.notifications.models import NotificationLevel
|
||||
from sientia_do.temporal.constants import DATETIME_FORMAT_MS_WITH_TZ
|
||||
|
||||
from orchestrator.activities.mongo_db import MongoDB
|
||||
|
||||
|
||||
@fixture
|
||||
@patch('orchestrator.activities.mongo_db.MongoDBRepository')
|
||||
def mongo_db(mongo_mock):
|
||||
mongo = MongoDB(
|
||||
connection_string='mongodb://localhost:27017',
|
||||
database_name='test_db',
|
||||
ttl_index_seconds=3600,
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
mongo.send_notification = MagicMock()
|
||||
mongo.emit_metric = AsyncMock()
|
||||
|
||||
return mongo
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.MongoDBRepository')
|
||||
def test___init__(mongo_mock):
|
||||
logger = MagicMock()
|
||||
notification_handler = MagicMock()
|
||||
metrics_controller = AsyncMock()
|
||||
|
||||
MongoDB(
|
||||
connection_string='mongodb://localhost:27017',
|
||||
database_name='test_db',
|
||||
ttl_index_seconds=3600,
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
|
||||
mongo_mock.assert_called_once_with(
|
||||
connection_string='mongodb://localhost:27017',
|
||||
database_name='test_db',
|
||||
logger=logger,
|
||||
notification_handler=notification_handler,
|
||||
metrics_controller=metrics_controller,
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.SientiaMonitoring')
|
||||
def test_close(sientia_monitoring_mock, mongo_db):
|
||||
mongo_db.close()
|
||||
mongo_db.mongo_db_repository.close.assert_called_once()
|
||||
sientia_monitoring_mock.shutdown.assert_called_once()
|
||||
|
||||
|
||||
def test___del__(mongo_db):
|
||||
mongo_db.close = MagicMock()
|
||||
mongo_db.__del__()
|
||||
mongo_db.close.assert_called_once()
|
||||
|
||||
|
||||
def test_find_documents_in_mongodb_success(mongo_db):
|
||||
input_data = {'collection': 'test_collection', 'filters': {'name': {'$exists': True}}}
|
||||
|
||||
mongo_db.mongo_db_repository.find = MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
'name': 'test1',
|
||||
'timestamp': datetime.strptime(
|
||||
'2023-01-01 12:00:00.000000+0000', DATETIME_FORMAT_MS_WITH_TZ
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'test2',
|
||||
'timestamp': datetime.strptime(
|
||||
'2023-01-01 12:00:00.000000+0000', DATETIME_FORMAT_MS_WITH_TZ
|
||||
),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
result = mongo_db.find_documents_in_mongodb(
|
||||
{'query': input_data, 'timestamp_fields': ['timestamp']}
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0] == {'name': 'test1', 'timestamp': '2023-01-01 12:00:00.000000+0000'}
|
||||
assert result[1] == {'name': 'test2', 'timestamp': '2023-01-01 12:00:00.000000+0000'}
|
||||
mongo_db.mongo_db_repository.find.assert_called_once_with(
|
||||
'test_collection', {'name': {'$exists': True}}, {}
|
||||
)
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test_schedule_name',
|
||||
'workflow_name': 'test_workflow_name',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_find_documents_in_mongodb_failure(mongo_db):
|
||||
input_data = {'collection': 'test_collection', 'filters': {'name': {'$exists': True}}}
|
||||
mongo_db.mongo_db_repository.find = MagicMock(side_effect=Exception('Error'))
|
||||
|
||||
try:
|
||||
mongo_db.find_documents_in_mongodb({'query': input_data, **metadata})
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_QUERY_ERROR',
|
||||
message='Failed to execute MongoDB query: Error',
|
||||
level=NotificationLevel.ERROR,
|
||||
block='load_query_from_mongodb',
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_find_documents_in_mongodb_missing_collection(mongo_db):
|
||||
input_data = {'query': {'filters': {}}}
|
||||
|
||||
try:
|
||||
mongo_db.find_documents_in_mongodb(input_data)
|
||||
|
||||
except ValueError as e:
|
||||
assert str(e) == 'Collection name must be provided in the query.'
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected a ValueError to be raised')
|
||||
|
||||
|
||||
def test_aggregate_documents_in_mongodb_success(mongo_db):
|
||||
input_data = {
|
||||
'collection': 'test_collection',
|
||||
'aggregation': [{'$match': {'name': {'$exists': True}}}, {'$project': {'name': 1}}],
|
||||
}
|
||||
mongo_db.mongo_db_repository.aggregate = MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
'name': 'test1',
|
||||
'timestamp': datetime.strptime(
|
||||
'2023-01-01 12:00:00.000000+0000', DATETIME_FORMAT_MS_WITH_TZ
|
||||
),
|
||||
},
|
||||
{
|
||||
'name': 'test2',
|
||||
'timestamp': datetime.strptime(
|
||||
'2023-01-01 12:00:00.000000+0000', DATETIME_FORMAT_MS_WITH_TZ
|
||||
),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
result = mongo_db.aggregate_documents_in_mongodb(
|
||||
{'query': input_data, 'timestamp_fields': ['timestamp']}
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0] == {'name': 'test1', 'timestamp': '2023-01-01 12:00:00.000000+0000'}
|
||||
assert result[1] == {'name': 'test2', 'timestamp': '2023-01-01 12:00:00.000000+0000'}
|
||||
expected_pipeline = input_data['aggregation']
|
||||
expected_pipeline.append({'$project': {'_id': 0}})
|
||||
|
||||
mongo_db.mongo_db_repository.aggregate.assert_called_once_with(
|
||||
'test_collection', expected_pipeline, {}
|
||||
)
|
||||
|
||||
|
||||
def test_aggregate_documents_in_mongodb_failure(mongo_db):
|
||||
input_data = {
|
||||
'collection': 'test_collection',
|
||||
'aggregation': [{'$match': {'name': {'$exists': True}}}, {'$project': {'name': 1}}],
|
||||
}
|
||||
mongo_db.mongo_db_repository.aggregate = MagicMock(side_effect=Exception('Error'))
|
||||
|
||||
try:
|
||||
mongo_db.aggregate_documents_in_mongodb({'query': input_data, **metadata})
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_AGGREGATION_ERROR',
|
||||
message='Failed to execute MongoDB aggregation: Error',
|
||||
block='aggregate_documents_in_mongodb',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_aggregate_documents_in_mongodb_missing_collection(mongo_db):
|
||||
input_data = {'query': {'aggregation': []}}
|
||||
|
||||
try:
|
||||
mongo_db.aggregate_documents_in_mongodb(input_data)
|
||||
|
||||
except ValueError as e:
|
||||
assert str(e) == 'Collection name must be provided in the query.'
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected a ValueError to be raised')
|
||||
|
||||
|
||||
def test_aggregate_documents_in_mongodb_missing_aggregation(mongo_db):
|
||||
input_data = {'query': {'collection': 'test_collection'}}
|
||||
|
||||
try:
|
||||
mongo_db.aggregate_documents_in_mongodb(input_data)
|
||||
|
||||
except ValueError as e:
|
||||
assert str(e) == 'Aggregation must be provided.'
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected a ValueError to be raised')
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_update_pipelines_timestamps_success(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'updated_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
]
|
||||
}
|
||||
mongo_db.mongo_db_repository.update_many = MagicMock(return_value=MagicMock())
|
||||
mongo_db.update_pipelines_timestamps(input_data)
|
||||
mongo_db.mongo_db_repository.update_many.assert_called_once_with(
|
||||
'orchestrated_schedules',
|
||||
{
|
||||
'$or': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1'},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2'},
|
||||
]
|
||||
},
|
||||
{'$set': {'updated_at': now_mock.return_value}},
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_update_pipelines_timestamps_failure(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'updated_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
],
|
||||
**metadata,
|
||||
}
|
||||
|
||||
mongo_db.mongo_db_repository.update_many.side_effect = Exception('Error')
|
||||
try:
|
||||
mongo_db.update_pipelines_timestamps(input_data)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_UPDATE_PIPELINES_ERROR',
|
||||
message='Failed to update pipelines timestamps: Error',
|
||||
block='update_pipelines_timestamps',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_create_pipelines_timestamps_success(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'created_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
]
|
||||
}
|
||||
mongo_db.mongo_db_repository.insert_many = MagicMock(return_value=MagicMock())
|
||||
mongo_db.create_pipelines_timestamps(input_data)
|
||||
mongo_db.mongo_db_repository.insert_many.assert_called_once_with(
|
||||
'orchestrated_schedules',
|
||||
[
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'updated_at': now_mock.return_value},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'updated_at': now_mock.return_value},
|
||||
],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_create_pipelines_timestamps_failure(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'created_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
],
|
||||
**metadata,
|
||||
}
|
||||
mongo_db.mongo_db_repository.insert_many.side_effect = Exception('Error')
|
||||
try:
|
||||
mongo_db.create_pipelines_timestamps(input_data)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_CREATE_PIPELINES_ERROR',
|
||||
message='Failed to create pipelines timestamps: Error',
|
||||
block='create_pipelines_timestamps',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_delete_pipelines_timestamps_success(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'deleted_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
]
|
||||
}
|
||||
mongo_db.mongo_db_repository.delete_many = MagicMock(return_value=MagicMock())
|
||||
mongo_db.delete_pipelines_timestamps(input_data)
|
||||
mongo_db.mongo_db_repository.delete_many.assert_called_once_with(
|
||||
'orchestrated_schedules',
|
||||
{
|
||||
'$or': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1'},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2'},
|
||||
]
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@patch('orchestrator.activities.mongo_db.now')
|
||||
def test_delete_pipelines_timestamps_failure(now_mock, mongo_db):
|
||||
input_data = {
|
||||
'deleted_pipelines': [
|
||||
{'schedule_name': 'test1', 'namespace': 'test1', 'success': True},
|
||||
{'schedule_name': 'test2', 'namespace': 'test2', 'success': True},
|
||||
],
|
||||
**metadata,
|
||||
}
|
||||
mongo_db.mongo_db_repository.delete_many.side_effect = Exception('Error')
|
||||
try:
|
||||
mongo_db.delete_pipelines_timestamps(input_data)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_DELETE_PIPELINES_ERROR',
|
||||
message='Failed to delete pipelines timestamps: Error',
|
||||
block='delete_pipelines_timestamps',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_create_collection_with_ttl_index_success(mongo_db):
|
||||
input_data = {
|
||||
**metadata,
|
||||
'pipelines': {
|
||||
'scouter-pipeline': {'topic': 'raw_scouter_pipeline'},
|
||||
'scouter-pipeline-2': {'topic': 'raw_scouter_pipeline_2'},
|
||||
'scouter-pipeline-3': {'topic': 'raw_scouter_pipeline_3'},
|
||||
},
|
||||
}
|
||||
|
||||
mongo_db.mongo_db_repository.database.list_collection_names = MagicMock(
|
||||
return_value=[
|
||||
'raw_scouter_pipeline_2',
|
||||
'raw_scouter_pipeline_3',
|
||||
]
|
||||
)
|
||||
|
||||
collection_1 = MagicMock(
|
||||
list_indexes=MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
'key': 'asdad',
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
collection_2 = MagicMock(
|
||||
list_indexes=MagicMock(return_value=[{'key': 'inserted_at', 'expireAfterSeconds': None}])
|
||||
)
|
||||
collection_3 = MagicMock(
|
||||
list_indexes=MagicMock(return_value=[{'key': 'inserted_at', 'expireAfterSeconds': 3600}])
|
||||
)
|
||||
|
||||
mongo_db.mongo_db_repository.database.__getitem__ = MagicMock(
|
||||
side_effect=[collection_1, collection_2, collection_3]
|
||||
)
|
||||
|
||||
mongo_db.create_collection_with_ttl_index(input_data)
|
||||
|
||||
mongo_db.mongo_db_repository.database.list_collection_names.assert_called_once_with()
|
||||
|
||||
mongo_db.mongo_db_repository.database.create_collection.assert_called_once_with(
|
||||
'raw_scouter_pipeline'
|
||||
)
|
||||
|
||||
collection_1.list_indexes.assert_called_once()
|
||||
collection_1.create_index.assert_called_once_with(
|
||||
'inserted_at', expireAfterSeconds=3600, background=True
|
||||
)
|
||||
|
||||
collection_2.list_indexes.assert_called_once()
|
||||
collection_2.create_index.assert_called_once_with(
|
||||
'inserted_at', expireAfterSeconds=3600, background=True
|
||||
)
|
||||
|
||||
collection_3.list_indexes.assert_called_once()
|
||||
collection_3.create_index.assert_not_called()
|
||||
|
||||
|
||||
def test_create_collection_with_ttl_index_failure(mongo_db):
|
||||
input_data = {**metadata, 'pipelines': {'scouter-pipeline': {'topic': 'raw_scouter_pipeline'}}}
|
||||
|
||||
mongo_db.mongo_db_repository.database.list_collection_names.return_value = []
|
||||
|
||||
mongo_db.mongo_db_repository.database.create_collection.side_effect = Exception('Error')
|
||||
|
||||
try:
|
||||
mongo_db.create_collection_with_ttl_index(input_data)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Error'
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='MONGODB_CREATE_COLLECTION_ERROR',
|
||||
message='Failed to create collection raw_scouter_pipeline with TTL index: Error',
|
||||
block='create_collection_with_ttl_index',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_load_latest_data_none_last_data_timestamp(mongo_db):
|
||||
"""Test load_latest_data"""
|
||||
mongo_db.mongo_db_repository.find = MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
'name': 'test1',
|
||||
'value': 1,
|
||||
'timestamp': '2023-01-01 12:00:00+0000',
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = mongo_db.load_latest_data(
|
||||
{
|
||||
'metadata': {'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
'collection_name': 'test_collection',
|
||||
'last_data_timestamp': None,
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
}
|
||||
)
|
||||
|
||||
mongo_db.mongo_db_repository.find.assert_called_once_with(
|
||||
'test_collection',
|
||||
{'level': 'ERROR'},
|
||||
{'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
)
|
||||
|
||||
assert result == [{'name': 'test1', 'value': 1, 'timestamp': '2023-01-01 12:00:00+0000'}]
|
||||
|
||||
|
||||
def test_load_latest_data_not_none_last_data_timestamp(mongo_db):
|
||||
"""Test load_latest_data"""
|
||||
|
||||
mongo_db.mongo_db_repository.find = MagicMock(
|
||||
return_value=[
|
||||
{
|
||||
'name': 'test1',
|
||||
'value': 1,
|
||||
'timestamp': '2023-01-01 12:00:00+0000',
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = mongo_db.load_latest_data(
|
||||
{
|
||||
'metadata': {'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
'collection_name': 'test_collection',
|
||||
'last_data_timestamp': '2023-01-01 12:00:00+0000',
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
}
|
||||
)
|
||||
|
||||
mongo_db.mongo_db_repository.find.assert_called_once_with(
|
||||
'test_collection',
|
||||
{
|
||||
'level': 'ERROR',
|
||||
'timestamp': {'$gt': '2023-01-01 12:00:00+0000'},
|
||||
},
|
||||
{'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
)
|
||||
|
||||
assert result == [{'name': 'test1', 'value': 1, 'timestamp': '2023-01-01 12:00:00+0000'}]
|
||||
|
||||
|
||||
def test_load_latest_data_error(mongo_db):
|
||||
"""Test load_latest_data"""
|
||||
mongo_db.mongo_db_repository.find.side_effect = Exception('test')
|
||||
|
||||
try:
|
||||
mongo_db.load_latest_data(
|
||||
{
|
||||
'metadata': {'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
'collection_name': 'test_collection',
|
||||
'last_data_timestamp': '2023-01-01 12:00:00+0000',
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
assert str(e) == 'test'
|
||||
|
||||
mongo_db.send_notification.assert_called_once_with(
|
||||
metadata={'workflow_name': 'test_pipeline', 'schedule_name': 'test_schedule'},
|
||||
notification_id='MONGO_LOAD_ERROR',
|
||||
message='Error loading data from MongoDB: test',
|
||||
block='load_latest_data',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
409
tests/orchestrator/activities/test_slot_manager.py
Normal file
409
tests/orchestrator/activities/test_slot_manager.py
Normal file
@@ -0,0 +1,409 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pandas import DataFrame
|
||||
from pytest import fixture
|
||||
from sientia_do.notifications.models import NotificationLevel
|
||||
from sientia_do.temporal.constants import DATETIME_FORMAT_MS_WITH_TZ, now
|
||||
|
||||
from orchestrator.activities.slot_manager import SlotManager
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test_schedule_name',
|
||||
'workflow_name': 'test_workflow_name',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@fixture
|
||||
@patch('orchestrator.activities.slot_manager.RedisRepository')
|
||||
def slot_manager(_redis_mock):
|
||||
slot_manager = SlotManager(
|
||||
host='localhost',
|
||||
port=6379,
|
||||
username='admin',
|
||||
password='password',
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
|
||||
slot_manager.redis_repository = MagicMock()
|
||||
slot_manager.logger = MagicMock()
|
||||
slot_manager.notification_handler = MagicMock()
|
||||
slot_manager.send_notification = MagicMock()
|
||||
slot_manager.emit_metric = AsyncMock()
|
||||
|
||||
return slot_manager
|
||||
|
||||
|
||||
def test_load_opc_slots_no_slot_keys(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(return_value=[])
|
||||
assert slot_manager.load_opc_slots(metadata) == {}
|
||||
|
||||
|
||||
def test_load_opc_slots(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(
|
||||
return_value=[
|
||||
b'slot:opc_tags:1',
|
||||
b'slot:opc_tags:2',
|
||||
b'slot:opc_tags:3',
|
||||
]
|
||||
)
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(side_effect=['value1', 'value2', None])
|
||||
|
||||
response = slot_manager.load_opc_slots(metadata)
|
||||
|
||||
assert response == {
|
||||
'slot:opc_tags:1': 'value1',
|
||||
'slot:opc_tags:2': 'value2',
|
||||
'slot:opc_tags:3': None,
|
||||
}
|
||||
|
||||
|
||||
def test_load_opc_slots_no_decode(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(
|
||||
return_value=[
|
||||
'slot:opc_tags:1',
|
||||
'slot:opc_tags:2',
|
||||
'slot:opc_tags:3',
|
||||
]
|
||||
)
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(side_effect=['value1', 'value2', None])
|
||||
|
||||
response = slot_manager.load_opc_slots(metadata)
|
||||
|
||||
assert response == {
|
||||
'slot:opc_tags:1': 'value1',
|
||||
'slot:opc_tags:2': 'value2',
|
||||
'slot:opc_tags:3': None,
|
||||
}
|
||||
|
||||
|
||||
def test_load_opc_slots_error(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(
|
||||
return_value=[
|
||||
'slot:opc_tags:1',
|
||||
'slot:opc_tags:2',
|
||||
'slot:opc_tags:3',
|
||||
]
|
||||
)
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(side_effect=Exception('Test exception'))
|
||||
|
||||
try:
|
||||
slot_manager.load_opc_slots(metadata)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Test exception'
|
||||
slot_manager.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='REDIS_GET_ERROR',
|
||||
message='Failed to load OPC slots: Test exception',
|
||||
block='load_opc_slots',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_load_active_ingestors(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(
|
||||
return_value=[
|
||||
b'heartbeat:ingestor:1',
|
||||
b'heartbeat:ingestor:2',
|
||||
'heartbeat:ingestor:3',
|
||||
]
|
||||
)
|
||||
|
||||
response = slot_manager.load_active_ingestors(metadata)
|
||||
|
||||
assert response == ['heartbeat:ingestor:1', 'heartbeat:ingestor:2', 'heartbeat:ingestor:3']
|
||||
|
||||
|
||||
def test_load_active_ingestors_error(slot_manager):
|
||||
slot_manager.redis_repository.keys = MagicMock(
|
||||
return_value=[
|
||||
'heartbeat:ingestor:1',
|
||||
'heartbeat:ingestor:2',
|
||||
'heartbeat:ingestor:3',
|
||||
]
|
||||
)
|
||||
|
||||
slot_manager.redis_repository.keys = MagicMock(side_effect=Exception('Test exception'))
|
||||
|
||||
try:
|
||||
slot_manager.load_active_ingestors(metadata)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Test exception'
|
||||
slot_manager.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='REDIS_GET_ERROR',
|
||||
message='Failed to load active ingestors: Test exception',
|
||||
block='load_active_ingestors',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
def test_update_slots(slot_manager):
|
||||
slot_manager.redis_repository.set = MagicMock(side_effect=[None, Exception('Test exception')])
|
||||
|
||||
response = slot_manager.update_slots({'to_insert': {'1': 'value1', '2': 'value2'}})
|
||||
|
||||
slot_manager.redis_repository.set.assert_has_calls(
|
||||
[call('slot:opc_tags:1', 'value1', ttl=None), call('slot:opc_tags:2', 'value2', ttl=None)]
|
||||
)
|
||||
|
||||
assert response == {
|
||||
'1': {'success': True, 'message': 'Slot updated successfully'},
|
||||
'2': {'success': False, 'message': 'Test exception'},
|
||||
}
|
||||
|
||||
|
||||
def test_delete_slots(slot_manager):
|
||||
slot_manager.redis_repository.delete = MagicMock(
|
||||
side_effect=[None, Exception('Test exception')]
|
||||
)
|
||||
|
||||
response = slot_manager.delete_slots({'to_delete': ['1', '2']})
|
||||
|
||||
slot_manager.redis_repository.delete.assert_has_calls(
|
||||
[call('slot:opc_tags:1'), call('slot:opc_tags:2')]
|
||||
)
|
||||
|
||||
assert response == {
|
||||
'1': {'success': True, 'message': 'Slot deleted successfully'},
|
||||
'2': {'success': False, 'message': 'Test exception'},
|
||||
}
|
||||
|
||||
|
||||
def test_get_last_data_timestamp_none(slot_manager):
|
||||
"""Test get_last_data_timestamp"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(return_value=None)
|
||||
|
||||
result = slot_manager.get_last_data_timestamp(test_data)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_last_data_timestamp_not_none(slot_manager):
|
||||
"""Test get_last_data_timestamp"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(return_value='2023-01-01 12:00:00')
|
||||
|
||||
result = slot_manager.get_last_data_timestamp(test_data)
|
||||
|
||||
slot_manager.redis_repository.get.assert_called_once_with(
|
||||
'notification_last_timestamp:test_mail_type'
|
||||
)
|
||||
|
||||
assert result == '2023-01-01 12:00:00'
|
||||
|
||||
|
||||
def test_get_last_data_timestamp_error(slot_manager):
|
||||
"""Test get_last_data_timestamp error"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.get = MagicMock(side_effect=Exception('test'))
|
||||
|
||||
try:
|
||||
slot_manager.get_last_data_timestamp(test_data)
|
||||
|
||||
except Exception as e:
|
||||
assert str(e) == 'test'
|
||||
|
||||
slot_manager.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='REDIS_GET_ERROR',
|
||||
message='Error getting last data timestamp: test',
|
||||
block='get_last_data_timestamp',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected exception')
|
||||
|
||||
|
||||
def test_put_last_data_timestamp_empty_dataframe(slot_manager):
|
||||
"""Test put_last_data_timestamp with empty dataframe"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'data': DataFrame(columns=['name', 'value', 'timestamp']).to_dict('records'),
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.set = MagicMock()
|
||||
|
||||
result = slot_manager.put_last_data_timestamp(test_data)
|
||||
|
||||
assert result is None
|
||||
|
||||
slot_manager.set.assert_not_called()
|
||||
|
||||
|
||||
def test_put_last_data_timestamp_not_empty_dataframe(slot_manager):
|
||||
"""Test put_last_data_timestamp with not empty dataframe"""
|
||||
|
||||
data = DataFrame(
|
||||
{
|
||||
'name': ['sensor1', 'sensor2'],
|
||||
'value': [25.5, 30.0],
|
||||
'timestamp': ['2023-01-01 12:00:00', '2023-01-01 12:00:01'],
|
||||
}
|
||||
)
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'data': data.to_dict('records'),
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.set = MagicMock()
|
||||
|
||||
result = slot_manager.put_last_data_timestamp(test_data)
|
||||
|
||||
assert result == '2023-01-01 12:00:01'
|
||||
|
||||
slot_manager.redis_repository.set.assert_called_once_with(
|
||||
'notification_last_timestamp:test_mail_type', '2023-01-01 12:00:01', ttl=18000
|
||||
)
|
||||
|
||||
|
||||
def test_put_last_data_timestamp_error(slot_manager):
|
||||
"""Test put_last_data_timestamp error"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'workflow_name': 'test_pipeline',
|
||||
'schedule_name': 'test_schedule',
|
||||
'data': DataFrame(
|
||||
{
|
||||
'name': ['sensor1', 'sensor2'],
|
||||
'value': [25.5, 30.0],
|
||||
'timestamp': ['2023-01-01 12:00:00'] * 2,
|
||||
}
|
||||
).to_dict('records'),
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.set = MagicMock(side_effect=Exception('test'))
|
||||
|
||||
try:
|
||||
slot_manager.put_last_data_timestamp(test_data)
|
||||
|
||||
except Exception as e:
|
||||
assert str(e) == 'test'
|
||||
|
||||
slot_manager.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='REDIS_SET_ERROR',
|
||||
message='Error setting last data timestamp: test',
|
||||
block='put_last_data_timestamp',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected exception')
|
||||
|
||||
|
||||
def test_filter_notification_alerts(slot_manager):
|
||||
slot_manager.redis_repository.get = MagicMock(
|
||||
side_effect=[
|
||||
None,
|
||||
(now() - timedelta(seconds=600)).strftime(DATETIME_FORMAT_MS_WITH_TZ),
|
||||
now().strftime(DATETIME_FORMAT_MS_WITH_TZ),
|
||||
None,
|
||||
(now() - timedelta(seconds=600)).strftime(DATETIME_FORMAT_MS_WITH_TZ),
|
||||
now().strftime(DATETIME_FORMAT_MS_WITH_TZ),
|
||||
]
|
||||
)
|
||||
|
||||
input_data = {
|
||||
**metadata,
|
||||
'notification_package': [
|
||||
{'trigger': 'test_trigger_1', 'notification_id': 'test_notification_id_1'},
|
||||
{'trigger': 'test_trigger_2', 'notification_id': 'test_notification_id_2'},
|
||||
{'trigger': 'test_trigger_3', 'notification_id': 'test_notification_id_3'},
|
||||
],
|
||||
'sending_configs': [
|
||||
{'group_name': 'test_group_1', 'contents': ['core_alerts', 'persistent_alerts']},
|
||||
{'group_name': 'test_group_2', 'contents': ['core_alerts']},
|
||||
],
|
||||
'notification_ttl': 300,
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
response = slot_manager.filter_notification_alerts(input_data)
|
||||
|
||||
assert response == {
|
||||
'test_group_1': {
|
||||
'group_name': 'test_group_1',
|
||||
'contents': ['core_alerts', 'persistent_alerts'],
|
||||
'notifications': [
|
||||
{'trigger': 'test_trigger_1', 'notification_id': 'test_notification_id_1'},
|
||||
{'trigger': 'test_trigger_2', 'notification_id': 'test_notification_id_2'},
|
||||
],
|
||||
},
|
||||
'test_group_2': {
|
||||
'group_name': 'test_group_2',
|
||||
'contents': ['core_alerts'],
|
||||
'notifications': [
|
||||
{'trigger': 'test_trigger_1', 'notification_id': 'test_notification_id_1'}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_store_notification_cache(slot_manager):
|
||||
"""Test store_notification_cache"""
|
||||
test_data = {
|
||||
**metadata,
|
||||
'log_report': DataFrame(
|
||||
{
|
||||
'status': ['sent', 'error'],
|
||||
'schedule': ['test_schedule_1', 'test_schedule_2'],
|
||||
'notification_id': ['test_notification_id_1', 'test_notification_id_2'],
|
||||
}
|
||||
).to_dict(),
|
||||
'sent_ttl': 600,
|
||||
}
|
||||
|
||||
slot_manager.redis_repository.set = MagicMock()
|
||||
|
||||
slot_manager.store_notification_cache(test_data)
|
||||
|
||||
slot_manager.redis_repository.set.assert_called_once_with(
|
||||
'test_schedule_1:test_notification_id_1', ANY, ttl=600
|
||||
)
|
||||
587
tests/orchestrator/activities/test_temporal_manager.py
Normal file
587
tests/orchestrator/activities/test_temporal_manager.py
Normal file
@@ -0,0 +1,587 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
from sientia_do.notifications.models import NotificationLevel
|
||||
|
||||
from orchestrator.activities.temporal_manager import TemporalManager
|
||||
from orchestrator.utils.converters import parse_frequency
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test_schedule_name',
|
||||
'workflow_name': 'test_workflow_name',
|
||||
'model_name': 'test_model_name',
|
||||
'model_id': 'test_model_id',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@fixture
|
||||
def temporal_manager():
|
||||
temporal_manager = TemporalManager(
|
||||
host='localhost:7233',
|
||||
scouter_namespace='scouter',
|
||||
laborious_namespace='laborious',
|
||||
logger=MagicMock(),
|
||||
notification_handler=MagicMock(),
|
||||
metrics_controller=AsyncMock(),
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'] = MagicMock()
|
||||
temporal_manager.temporal_clients['laborious'] = MagicMock()
|
||||
temporal_manager.send_notification_async = AsyncMock()
|
||||
temporal_manager.emit_metric = AsyncMock()
|
||||
temporal_manager.send_notification = MagicMock()
|
||||
|
||||
return temporal_manager
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.activities.temporal_manager.Client.connect', new_callable=AsyncMock)
|
||||
async def test_connect_to_temporal(connect_mock, temporal_manager):
|
||||
await temporal_manager.connect_to_temporal()
|
||||
connect_mock.assert_has_calls(
|
||||
[
|
||||
call(target_host='localhost:7233', namespace='scouter'),
|
||||
call(target_host='localhost:7233', namespace='laborious'),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_iter():
|
||||
yield MagicMock(id='test-schedule-id', search_attributes={'orchestrated': ['true']})
|
||||
yield MagicMock(id='test-schedule-id-2', search_attributes={'Attr': ['false']})
|
||||
yield MagicMock(id='test-schedule-id-3', search_attributes={'Attr': ['false']})
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_normalize_schedules(temporal_manager):
|
||||
input_data = {
|
||||
'orchestrated_schedules': {
|
||||
'scouter': {'test-scouter': '2021-01-01'},
|
||||
'laborious': {'test-schedule-id1': '2021-01-01'},
|
||||
},
|
||||
**metadata,
|
||||
}
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].list_schedules = AsyncMock(
|
||||
return_value=async_iter()
|
||||
)
|
||||
temporal_manager.temporal_clients['laborious'].list_schedules = AsyncMock(
|
||||
return_value=async_iter()
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].get_schedule_handle = MagicMock(
|
||||
return_value=MagicMock(delete=AsyncMock())
|
||||
)
|
||||
temporal_manager.temporal_clients['laborious'].get_schedule_handle = MagicMock(
|
||||
return_value=MagicMock(delete=AsyncMock())
|
||||
)
|
||||
|
||||
await temporal_manager.normalize_schedules(input_data)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].get_schedule_handle.assert_has_calls(
|
||||
[
|
||||
call('test-schedule-id'),
|
||||
]
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients[
|
||||
'scouter'
|
||||
].get_schedule_handle.return_value.delete.assert_awaited_once()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_normalize_schedules_error(temporal_manager):
|
||||
temporal_manager.temporal_clients['scouter'].list_schedules = AsyncMock(
|
||||
side_effect=Exception('Test exception')
|
||||
)
|
||||
|
||||
try:
|
||||
await temporal_manager.normalize_schedules(metadata)
|
||||
except Exception as e:
|
||||
assert str(e) == 'Test exception'
|
||||
temporal_manager.send_notification_async.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
notification_id='SCHEDULER_NORMALIZE_SCHEDULES_ERROR',
|
||||
message='Failed to normalize schedules: Test exception',
|
||||
block='normalize_schedules',
|
||||
level=NotificationLevel.ERROR,
|
||||
attachment_content=ANY,
|
||||
)
|
||||
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.activities.temporal_manager.parse_frequency', side_effect=parse_frequency)
|
||||
@patch('orchestrator.activities.temporal_manager.Schedule')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleActionStartWorkflow')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleIntervalSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.TypedSearchAttributes')
|
||||
@patch('orchestrator.activities.temporal_manager.SearchAttributePair')
|
||||
async def test_create_schedule(
|
||||
mock_search_attribute_pair,
|
||||
mock_typed_search_attributes,
|
||||
mock_schedule_spec,
|
||||
mock_schedule_interval_spec,
|
||||
mock_schedule_action_start_workflow,
|
||||
mock_schedule,
|
||||
mock_parse_frequency,
|
||||
temporal_manager,
|
||||
):
|
||||
input_data = {
|
||||
'schedules': {
|
||||
'scouter': {
|
||||
'test-schedule': {
|
||||
'model_id': 1,
|
||||
'model_name': 'test-model-name',
|
||||
'workflow_type': 'test-workflow',
|
||||
'frequency': '1m',
|
||||
'data': {'test': 'test'},
|
||||
'execution_timeout_seconds': 100,
|
||||
'task_timeout_seconds': 100,
|
||||
},
|
||||
'test-schedule-invalid-frequency': {
|
||||
'model_id': 2,
|
||||
'model_name': 'test-model-name',
|
||||
'workflow_type': 'test-workflow',
|
||||
'frequency': '10y',
|
||||
'offset': '5m',
|
||||
'data': {'test': 'test'},
|
||||
'execution_timeout_seconds': 400,
|
||||
'task_timeout_seconds': 400,
|
||||
},
|
||||
},
|
||||
'laborious': {
|
||||
'test-schedule-laborious': {
|
||||
'model_id': 1,
|
||||
'model_name': 'test-model-name',
|
||||
'workflow_type': 'test-workflow',
|
||||
'frequency': '2m',
|
||||
'offset': '1h',
|
||||
'data': {'test': 'test'},
|
||||
'execution_timeout_seconds': 500,
|
||||
'task_timeout_seconds': 500,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].create_schedule = AsyncMock()
|
||||
temporal_manager.temporal_clients['laborious'].create_schedule = AsyncMock()
|
||||
|
||||
report = await temporal_manager.create_schedules(input_data)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].create_schedule.assert_called_once_with(
|
||||
'test-schedule',
|
||||
mock_schedule.return_value,
|
||||
search_attributes=mock_typed_search_attributes.return_value,
|
||||
)
|
||||
temporal_manager.temporal_clients['laborious'].create_schedule.assert_called_once_with(
|
||||
'test-schedule-laborious',
|
||||
mock_schedule.return_value,
|
||||
search_attributes=mock_typed_search_attributes.return_value,
|
||||
)
|
||||
|
||||
mock_schedule.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
action=mock_schedule_action_start_workflow.return_value,
|
||||
spec=mock_schedule_spec.return_value,
|
||||
),
|
||||
call(
|
||||
action=mock_schedule_action_start_workflow.return_value,
|
||||
spec=mock_schedule_spec.return_value,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
mock_schedule_action_start_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'test-workflow',
|
||||
input_data['schedules']['scouter']['test-schedule'],
|
||||
id='test-schedule',
|
||||
task_queue='test-workflow-legacy-queue',
|
||||
execution_timeout=timedelta(seconds=100),
|
||||
run_timeout=timedelta(seconds=100),
|
||||
task_timeout=timedelta(seconds=100),
|
||||
typed_search_attributes=mock_typed_search_attributes.return_value,
|
||||
),
|
||||
call(
|
||||
'test-workflow',
|
||||
input_data['schedules']['scouter']['test-schedule-invalid-frequency'],
|
||||
id='test-schedule-invalid-frequency',
|
||||
task_queue='test-workflow-legacy-queue',
|
||||
execution_timeout=timedelta(seconds=400),
|
||||
run_timeout=timedelta(seconds=400),
|
||||
task_timeout=timedelta(seconds=400),
|
||||
typed_search_attributes=mock_typed_search_attributes.return_value,
|
||||
),
|
||||
call(
|
||||
'test-workflow',
|
||||
input_data['schedules']['laborious']['test-schedule-laborious'],
|
||||
id='test-schedule-laborious',
|
||||
task_queue='test-workflow-legacy-queue',
|
||||
execution_timeout=timedelta(seconds=500),
|
||||
run_timeout=timedelta(seconds=500),
|
||||
task_timeout=timedelta(seconds=500),
|
||||
typed_search_attributes=mock_typed_search_attributes.return_value,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
mock_schedule_spec.assert_has_calls(
|
||||
[
|
||||
call(intervals=[mock_schedule_interval_spec.return_value]),
|
||||
call(intervals=[mock_schedule_interval_spec.return_value]),
|
||||
]
|
||||
)
|
||||
|
||||
mock_schedule_interval_spec.assert_has_calls(
|
||||
[
|
||||
call(every=timedelta(seconds=60), offset=timedelta(seconds=0)),
|
||||
call(every=timedelta(seconds=120), offset=timedelta(seconds=3600)),
|
||||
]
|
||||
)
|
||||
|
||||
mock_parse_frequency.assert_has_calls(
|
||||
[call('1m'), call('0m'), call('10y'), call('2m'), call('1h')]
|
||||
)
|
||||
|
||||
mock_typed_search_attributes.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
]
|
||||
),
|
||||
call(
|
||||
[
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
]
|
||||
),
|
||||
call(
|
||||
[
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
mock_search_attribute_pair.return_value,
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
mock_search_attribute_pair.assert_has_calls(
|
||||
[
|
||||
call(key=temporal_manager.model_id_id_key, value=1),
|
||||
call(key=temporal_manager.model_name_id_key, value='test-model-name'),
|
||||
call(key=temporal_manager.orchestrated_id_key, value='true'),
|
||||
call(key=temporal_manager.model_id_id_key, value=2),
|
||||
call(key=temporal_manager.model_name_id_key, value='test-model-name'),
|
||||
call(key=temporal_manager.orchestrated_id_key, value='true'),
|
||||
]
|
||||
)
|
||||
|
||||
assert report == [
|
||||
{
|
||||
'schedule_name': 'test-schedule',
|
||||
'namespace': 'scouter',
|
||||
'success': True,
|
||||
'message': 'Schedule created successfully',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule-invalid-frequency',
|
||||
'namespace': 'scouter',
|
||||
'success': False,
|
||||
'message': 'Invalid frequency',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule-laborious',
|
||||
'namespace': 'laborious',
|
||||
'success': True,
|
||||
'message': 'Schedule created successfully',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_create_schedules_with_no_client(temporal_manager):
|
||||
temporal_manager.temporal_clients = {}
|
||||
input_data = {
|
||||
'schedules': {'abc': {'test-schedule': {'frequency': '1m', 'data': {'test': 'test'}}}}
|
||||
}
|
||||
|
||||
try:
|
||||
await temporal_manager.create_schedules(input_data)
|
||||
except Exception as e:
|
||||
assert (
|
||||
str(e)
|
||||
== f'Temporal client for abc not found, clients: {temporal_manager.temporal_clients}'
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.activities.temporal_manager.parse_frequency', side_effect=parse_frequency)
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleIntervalSpec')
|
||||
async def test_update_schedules(
|
||||
_mock_schedule_interval_spec, _mock_parse_frequency, temporal_manager
|
||||
):
|
||||
input_mock = MagicMock(args=MagicMock())
|
||||
temporal_manager.schedule_handles = {
|
||||
'scouter': {
|
||||
'test-schedule': MagicMock(
|
||||
update=AsyncMock(update=AsyncMock(side_effect=lambda f: f(input_mock)))
|
||||
)
|
||||
},
|
||||
'laborious': {
|
||||
'test-schedule-laborious': MagicMock(
|
||||
update=AsyncMock(update=AsyncMock(side_effect=lambda f: f(input_mock)))
|
||||
)
|
||||
},
|
||||
}
|
||||
input_data = {
|
||||
'schedules': {
|
||||
'scouter': {
|
||||
'test-schedule': {'frequency': '1m', 'data': {'test': 'test'}},
|
||||
'test-schedule_no_handler': {'frequency': '1m', 'data': {'test': 'test'}},
|
||||
},
|
||||
'laborious': {'test-schedule-laborious': {'frequency': '2m', 'data': {'test': 'test'}}},
|
||||
}
|
||||
}
|
||||
|
||||
handler_scouter = MagicMock(
|
||||
update=AsyncMock(update=AsyncMock(side_effect=lambda f: f(input_mock)))
|
||||
)
|
||||
|
||||
handler_laborious = MagicMock(
|
||||
update=AsyncMock(update=AsyncMock(side_effect=lambda f: f(input_mock)))
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].get_schedule_handle = MagicMock(
|
||||
side_effect=[handler_scouter, None]
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients['laborious'].get_schedule_handle = MagicMock(
|
||||
side_effect=[handler_laborious]
|
||||
)
|
||||
|
||||
report = await temporal_manager.update_schedules(input_data)
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].get_schedule_handle.assert_has_calls(
|
||||
[call('test-schedule'), call('test-schedule_no_handler')]
|
||||
)
|
||||
temporal_manager.temporal_clients['laborious'].get_schedule_handle.assert_has_calls(
|
||||
[call('test-schedule-laborious')]
|
||||
)
|
||||
|
||||
handler_scouter.update.assert_called_once()
|
||||
handler_laborious.update.assert_called_once()
|
||||
|
||||
assert report == [
|
||||
{
|
||||
'schedule_name': 'test-schedule',
|
||||
'namespace': 'scouter',
|
||||
'success': True,
|
||||
'message': 'Schedule updated successfully',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule_no_handler',
|
||||
'namespace': 'scouter',
|
||||
'success': False,
|
||||
'message': 'Schedule test-schedule_no_handler not found',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule-laborious',
|
||||
'namespace': 'laborious',
|
||||
'success': True,
|
||||
'message': 'Schedule updated successfully',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_update_schedules_with_no_client(temporal_manager):
|
||||
temporal_manager.temporal_clients = {}
|
||||
input_data = {
|
||||
'schedules': {'abc': {'test-schedule': {'frequency': '1m', 'data': {'test': 'test'}}}}
|
||||
}
|
||||
|
||||
try:
|
||||
await temporal_manager.update_schedules(input_data)
|
||||
except Exception as e:
|
||||
assert (
|
||||
str(e)
|
||||
== f'Temporal client for abc not found, clients: {temporal_manager.temporal_clients}'
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_delete_schedules(temporal_manager):
|
||||
temporal_manager.schedule_handles = {
|
||||
'scouter': {'test-schedule': MagicMock(delete=AsyncMock())},
|
||||
'laborious': {'test-schedule-laborious': MagicMock(delete=AsyncMock())},
|
||||
}
|
||||
|
||||
input_data = {
|
||||
'schedules': {
|
||||
'scouter': ['test-schedule', 'test-schedule_no_handler'],
|
||||
'laborious': ['test-schedule-laborious'],
|
||||
}
|
||||
}
|
||||
handler_scouter = MagicMock(delete=AsyncMock())
|
||||
|
||||
handler_laborious = MagicMock(delete=AsyncMock())
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].get_schedule_handle = MagicMock(
|
||||
side_effect=[handler_scouter, None]
|
||||
)
|
||||
|
||||
temporal_manager.temporal_clients['laborious'].get_schedule_handle = MagicMock(
|
||||
side_effect=[handler_laborious]
|
||||
)
|
||||
|
||||
report = await temporal_manager.delete_schedules(input_data)
|
||||
|
||||
handler_scouter.delete.assert_called_once()
|
||||
handler_laborious.delete.assert_called_once()
|
||||
|
||||
assert report == [
|
||||
{
|
||||
'schedule_name': 'test-schedule',
|
||||
'namespace': 'scouter',
|
||||
'success': True,
|
||||
'message': 'Schedule deleted successfully',
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule_no_handler',
|
||||
'namespace': 'scouter',
|
||||
'success': False,
|
||||
'message': 'Schedule test-schedule_no_handler not found',
|
||||
'attachment': ANY,
|
||||
},
|
||||
{
|
||||
'schedule_name': 'test-schedule-laborious',
|
||||
'namespace': 'laborious',
|
||||
'success': True,
|
||||
'message': 'Schedule deleted successfully',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
async def test_delete_schedules_with_no_client(temporal_manager):
|
||||
temporal_manager.temporal_clients = {}
|
||||
input_data = {'schedules': {'abc': ['test-schedule']}}
|
||||
|
||||
try:
|
||||
await temporal_manager.delete_schedules(input_data)
|
||||
except Exception as e:
|
||||
assert (
|
||||
str(e)
|
||||
== f'Temporal client for abc not found, clients: {temporal_manager.temporal_clients}'
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.activities.temporal_manager.parse_frequency', side_effect=parse_frequency)
|
||||
@patch('orchestrator.activities.temporal_manager.Schedule')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleActionStartWorkflow')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleIntervalSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.TypedSearchAttributes')
|
||||
@patch('orchestrator.activities.temporal_manager.SearchAttributePair')
|
||||
async def test_create_schedules_default_runtime_legacy_queue(
|
||||
_mock_search_attribute_pair,
|
||||
_mock_typed_search_attributes,
|
||||
_mock_schedule_spec,
|
||||
_mock_schedule_interval_spec,
|
||||
mock_schedule_action_start_workflow,
|
||||
_mock_schedule,
|
||||
_mock_parse_frequency,
|
||||
temporal_manager,
|
||||
):
|
||||
input_data = {
|
||||
'schedules': {
|
||||
'scouter': {
|
||||
'test-schedule': {
|
||||
'model_id': 1,
|
||||
'model_name': 'test-model-name',
|
||||
'workflow_type': 'scouter',
|
||||
'frequency': '1m',
|
||||
'data': {'test': 'test'},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].create_schedule = AsyncMock()
|
||||
|
||||
await temporal_manager.create_schedules(input_data)
|
||||
|
||||
mock_schedule_action_start_workflow.assert_called_once_with(
|
||||
'scouter',
|
||||
input_data['schedules']['scouter']['test-schedule'],
|
||||
id='test-schedule',
|
||||
task_queue='scouter-legacy-queue',
|
||||
execution_timeout=timedelta(seconds=300),
|
||||
run_timeout=timedelta(seconds=300),
|
||||
task_timeout=timedelta(seconds=300),
|
||||
typed_search_attributes=_mock_typed_search_attributes.return_value,
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.activities.temporal_manager.parse_frequency', side_effect=parse_frequency)
|
||||
@patch('orchestrator.activities.temporal_manager.Schedule')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleActionStartWorkflow')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleIntervalSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.ScheduleSpec')
|
||||
@patch('orchestrator.activities.temporal_manager.TypedSearchAttributes')
|
||||
@patch('orchestrator.activities.temporal_manager.SearchAttributePair')
|
||||
async def test_create_schedules_tenant_runtime_queue(
|
||||
_mock_search_attribute_pair,
|
||||
_mock_typed_search_attributes,
|
||||
_mock_schedule_spec,
|
||||
_mock_schedule_interval_spec,
|
||||
mock_schedule_action_start_workflow,
|
||||
_mock_schedule,
|
||||
_mock_parse_frequency,
|
||||
temporal_manager,
|
||||
):
|
||||
input_data = {
|
||||
'schedules': {
|
||||
'scouter': {
|
||||
'test-schedule': {
|
||||
'model_id': 1,
|
||||
'model_name': 'test-model-name',
|
||||
'workflow_type': 'scouter',
|
||||
'frequency': '1m',
|
||||
'runtime': 'tenant-x',
|
||||
'data': {'test': 'test'},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
temporal_manager.temporal_clients['scouter'].create_schedule = AsyncMock()
|
||||
|
||||
await temporal_manager.create_schedules(input_data)
|
||||
|
||||
mock_schedule_action_start_workflow.assert_called_once_with(
|
||||
'scouter',
|
||||
input_data['schedules']['scouter']['test-schedule'],
|
||||
id='test-schedule',
|
||||
task_queue='scouter-tenant-x-queue',
|
||||
execution_timeout=timedelta(seconds=300),
|
||||
run_timeout=timedelta(seconds=300),
|
||||
task_timeout=timedelta(seconds=300),
|
||||
typed_search_attributes=_mock_typed_search_attributes.return_value,
|
||||
)
|
||||
0
tests/orchestrator/utils/__init__.py
Normal file
0
tests/orchestrator/utils/__init__.py
Normal file
174
tests/orchestrator/utils/test_connectors_config.py
Normal file
174
tests/orchestrator/utils/test_connectors_config.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from os import environ
|
||||
|
||||
from orchestrator.utils.connectors_config import (
|
||||
build_couchbase_config,
|
||||
build_email_config,
|
||||
build_mongodb_config,
|
||||
build_postgres_config,
|
||||
build_redis_config,
|
||||
build_temporal_config,
|
||||
)
|
||||
|
||||
|
||||
def test_build_redis_config_with_env_vars():
|
||||
environ['REDIS_HOST'] = 'localhost'
|
||||
environ['REDIS_PORT'] = '6379'
|
||||
environ['REDIS_USERNAME'] = 'sientia'
|
||||
environ['REDIS_PASSWORD'] = 'sientia'
|
||||
assert build_redis_config() == {
|
||||
'host': 'localhost',
|
||||
'port': 6379,
|
||||
'username': 'sientia',
|
||||
'password': 'sientia',
|
||||
}
|
||||
|
||||
|
||||
def test_build_couchbase_config_with_env_vars():
|
||||
environ['COUCHBASE_CONNECTION_STRING'] = 'couchbase://localhost'
|
||||
environ['COUCHBASE_USERNAME'] = 'sientia'
|
||||
environ['COUCHBASE_PASSWORD'] = 'sientia'
|
||||
assert build_couchbase_config() == {
|
||||
'connection_string': 'couchbase://localhost',
|
||||
'username': 'sientia',
|
||||
'password': 'sientia',
|
||||
}
|
||||
|
||||
|
||||
def test_build_redis_config_with_defaults():
|
||||
environ.pop('REDIS_HOST', None)
|
||||
environ.pop('REDIS_PORT', None)
|
||||
environ.pop('REDIS_USERNAME', None)
|
||||
environ.pop('REDIS_PASSWORD', None)
|
||||
assert build_redis_config() == {
|
||||
'host': 'localhost',
|
||||
'port': 6379,
|
||||
'username': 'default',
|
||||
'password': 'bdnZOpcyiL',
|
||||
}
|
||||
|
||||
|
||||
def test_build_couchbase_config_with_defaults():
|
||||
environ.pop('COUCHBASE_CONNECTION_STRING', None)
|
||||
environ.pop('COUCHBASE_USERNAME', None)
|
||||
environ.pop('COUCHBASE_PASSWORD', None)
|
||||
assert build_couchbase_config() == {
|
||||
'connection_string': 'couchbase://localhost',
|
||||
'username': 'sientia',
|
||||
'password': 'sientia',
|
||||
}
|
||||
|
||||
|
||||
def test_build_mongo_db_config_with_env_vars():
|
||||
environ['MONGODB_USERNAME'] = 'sientia1'
|
||||
environ['MONGODB_PASSWORD'] = 'sientia1'
|
||||
environ['MONGODB_URL'] = 'localhost:27018'
|
||||
environ['MONGODB_DATABASE_NAME'] = 'test_db'
|
||||
environ['MONGODB_TTL_INDEX_HOURS'] = '2'
|
||||
assert build_mongodb_config() == {
|
||||
'connection_string': 'mongodb://sientia1:sientia1@localhost:27018',
|
||||
'database_name': 'test_db',
|
||||
'ttl_index_seconds': 7200,
|
||||
}
|
||||
|
||||
|
||||
def test_build_mongo_db_config_with_defaults():
|
||||
environ.pop('MONGODB_USERNAME', None)
|
||||
environ.pop('MONGODB_PASSWORD', None)
|
||||
environ.pop('MONGODB_DATABASE_NAME', None)
|
||||
environ.pop('MONGODB_URL', None)
|
||||
environ.pop('MONGODB_TTL_INDEX_HOURS', None)
|
||||
assert build_mongodb_config() == {
|
||||
'connection_string': 'mongodb://root:wKZDbMNU1c@localhost:27018',
|
||||
'database_name': 'sientia',
|
||||
'ttl_index_seconds': 3600,
|
||||
}
|
||||
|
||||
|
||||
def test_build_temporal_config_with_env_vars():
|
||||
environ['TEMPORAL_HOST'] = 'localhost:7233'
|
||||
environ['TEMPORAL_SCOUTER_NAMESPACE'] = 'scouter'
|
||||
environ['TEMPORAL_LABORIOUS_NAMESPACE'] = 'laborious'
|
||||
assert build_temporal_config() == {
|
||||
'temporal_host': 'localhost:7233',
|
||||
'temporal_namespace': 'default',
|
||||
'temporal_scouter_namespace': 'scouter',
|
||||
'temporal_laborious_namespace': 'laborious',
|
||||
}
|
||||
|
||||
|
||||
def test_build_temporal_config_with_defaults():
|
||||
environ.pop('TEMPORAL_HOST', None)
|
||||
environ.pop('TEMPORAL_SCOUTER_NAMESPACE', None)
|
||||
environ.pop('TEMPORAL_LABORIOUS_NAMESPACE', None)
|
||||
assert build_temporal_config() == {
|
||||
'temporal_host': 'localhost:7233',
|
||||
'temporal_namespace': 'default',
|
||||
'temporal_scouter_namespace': 'scouter',
|
||||
'temporal_laborious_namespace': 'laborious',
|
||||
}
|
||||
|
||||
|
||||
def test_build_email_config_with_env_vars():
|
||||
environ['EMAIL_SENDER'] = 'test@test.com'
|
||||
environ['EMAIL_SENDER_PASSWORD'] = 'test'
|
||||
environ['EMAIL_SMTP_SERVER'] = 'test'
|
||||
environ['EMAIL_SMTP_PORT'] = '587'
|
||||
assert build_email_config() == {
|
||||
'sender_email': 'test@test.com',
|
||||
'sender_password': 'test',
|
||||
'smtp_server': 'test',
|
||||
'smtp_port': 587,
|
||||
}
|
||||
|
||||
|
||||
def test_build_email_config_with_defaults():
|
||||
environ.pop('EMAIL_SENDER', None)
|
||||
environ.pop('EMAIL_SENDER_PASSWORD', None)
|
||||
environ.pop('EMAIL_SMTP_SERVER', None)
|
||||
environ.pop('EMAIL_SMTP_PORT', None)
|
||||
|
||||
assert build_email_config() == {
|
||||
'sender_email': 'sientia-alerts@aignosi.com',
|
||||
'sender_password': 'sientia',
|
||||
'smtp_server': None,
|
||||
'smtp_port': 587,
|
||||
}
|
||||
|
||||
|
||||
def test_build_postgres_config_with_env_vars():
|
||||
environ['POSTGRES_HOST'] = 'localhost'
|
||||
environ['POSTGRES_PORT'] = '5432'
|
||||
environ['POSTGRES_USER'] = 'sientia'
|
||||
environ['POSTGRES_PASSWORD'] = 'sientia'
|
||||
environ['POSTGRES_DBNAME'] = 'sientia'
|
||||
environ['POSTGRES_MIN_CONNECTIONS'] = '5'
|
||||
environ['POSTGRES_MAX_CONNECTIONS'] = '20'
|
||||
assert build_postgres_config() == {
|
||||
'host': 'localhost',
|
||||
'port': 5432,
|
||||
'user': 'sientia',
|
||||
'password': 'sientia',
|
||||
'dbname': 'sientia',
|
||||
'min_connections': 5,
|
||||
'max_connections': 20,
|
||||
}
|
||||
|
||||
|
||||
def test_build_postgres_config_with_defaults():
|
||||
environ.pop('POSTGRES_HOST', None)
|
||||
environ.pop('POSTGRES_PORT', None)
|
||||
environ.pop('POSTGRES_USER', None)
|
||||
environ.pop('POSTGRES_PASSWORD', None)
|
||||
environ.pop('POSTGRES_DBNAME', None)
|
||||
environ.pop('POSTGRES_MIN_CONNECTIONS', None)
|
||||
environ.pop('POSTGRES_MAX_CONNECTIONS', None)
|
||||
|
||||
assert build_postgres_config() == {
|
||||
'host': 'localhost',
|
||||
'port': 5432,
|
||||
'user': 'sientia',
|
||||
'password': 'sientia',
|
||||
'dbname': 'sientia',
|
||||
'min_connections': 5,
|
||||
'max_connections': 20,
|
||||
}
|
||||
15
tests/orchestrator/utils/test_converters.py
Normal file
15
tests/orchestrator/utils/test_converters.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from orchestrator.utils.converters import parse_frequency
|
||||
|
||||
|
||||
def test_parse_frequency():
|
||||
assert parse_frequency('1s') == 1
|
||||
assert parse_frequency('1m') == 60
|
||||
assert parse_frequency('1h') == 60 * 60
|
||||
assert parse_frequency('1d') == 60 * 60 * 24
|
||||
|
||||
try:
|
||||
parse_frequency('1')
|
||||
except ValueError as e:
|
||||
assert str(e) == 'Invalid frequency'
|
||||
else:
|
||||
raise AssertionError('Expected an exception to be raised')
|
||||
198
tests/orchestrator/utils/test_email_builder.py
Normal file
198
tests/orchestrator/utils/test_email_builder.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from orchestrator.utils.email_builder import EmailBuilder
|
||||
|
||||
|
||||
@fixture
|
||||
@patch('orchestrator.utils.email_builder.open')
|
||||
def report_builder(open_mock):
|
||||
return EmailBuilder(MagicMock())
|
||||
|
||||
|
||||
@patch('orchestrator.utils.email_builder.Template')
|
||||
def test_replace_parameters(template, report_builder):
|
||||
output = report_builder.replace_parameters('template', {'key': 'value'})
|
||||
|
||||
assert output == template.return_value.render.return_value
|
||||
|
||||
template.assert_called_once_with('template')
|
||||
template.return_value.render.assert_called_once_with({'key': 'value'})
|
||||
|
||||
|
||||
def test_parameters(report_builder):
|
||||
report_builder.replace_parameters = MagicMock()
|
||||
general_events = {
|
||||
'ERROR': {
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [{'notification_id': 'ID_1', 'level': 'ERROR', 'project': 'project'}],
|
||||
},
|
||||
]
|
||||
},
|
||||
'WARNING': {
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [
|
||||
{'notification_id': 'ID_2', 'level': 'WARNING', 'project': 'project'}
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
'INFO': {
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [{'notification_id': 'ID_3', 'level': 'INFO', 'project': 'project'}],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
output = report_builder.parameters(general_events, 'model_name')
|
||||
|
||||
assert output == {
|
||||
'mail_type': 'model_name',
|
||||
'error_events': report_builder.replace_parameters.return_value,
|
||||
'warning_events': report_builder.replace_parameters.return_value,
|
||||
'info_events': report_builder.replace_parameters.return_value,
|
||||
}
|
||||
|
||||
report_builder.replace_parameters.assert_any_call(
|
||||
report_builder.general_template,
|
||||
{
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [{'notification_id': 'ID_1', 'level': 'ERROR', 'project': 'project'}],
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
report_builder.replace_parameters.assert_any_call(
|
||||
report_builder.general_template,
|
||||
{
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [
|
||||
{'notification_id': 'ID_2', 'level': 'WARNING', 'project': 'project'}
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
report_builder.replace_parameters.assert_any_call(
|
||||
report_builder.general_template,
|
||||
{
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [{'notification_id': 'ID_3', 'level': 'INFO', 'project': 'project'}],
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_build_email(report_builder):
|
||||
report_builder.parameters = MagicMock()
|
||||
report_builder.replace_parameters = MagicMock()
|
||||
|
||||
report_data = [
|
||||
{
|
||||
'notification_id': 'ID_1',
|
||||
'level': 'ERROR',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
{
|
||||
'notification_id': 'ID_2',
|
||||
'level': 'WARNING',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
{
|
||||
'notification_id': 'ID_2',
|
||||
'level': 'INFO',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
{
|
||||
'notification_id': 'ID_3',
|
||||
'level': 'ERROR',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
]
|
||||
|
||||
html = report_builder.build_email(report_data, 'type_1')
|
||||
|
||||
report_builder.replace_parameters.assert_called_once_with(
|
||||
report_builder.report_template, report_builder.parameters.return_value
|
||||
)
|
||||
|
||||
assert html == report_builder.replace_parameters.return_value
|
||||
|
||||
report_builder.parameters.assert_called_once_with(
|
||||
{
|
||||
'ERROR': {
|
||||
'section_name': 'Errors detected:',
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [
|
||||
{
|
||||
'notification_id': 'ID_1',
|
||||
'level': 'ERROR',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
{
|
||||
'notification_id': 'ID_3',
|
||||
'level': 'ERROR',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
'WARNING': {
|
||||
'section_name': 'Warnings detected:',
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [
|
||||
{
|
||||
'notification_id': 'ID_2',
|
||||
'level': 'WARNING',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
'INFO': {
|
||||
'section_name': 'Infos detected:',
|
||||
'models': [
|
||||
{
|
||||
'model_name': 'model_name',
|
||||
'events': [
|
||||
{
|
||||
'notification_id': 'ID_2',
|
||||
'level': 'INFO',
|
||||
'project': 'project',
|
||||
'model_name': 'model_name',
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
'type_1',
|
||||
)
|
||||
764
tests/orchestrator/utils/test_orchestrator_functions.py
Normal file
764
tests/orchestrator/utils/test_orchestrator_functions.py
Normal file
@@ -0,0 +1,764 @@
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from orchestrator.utils.orchestrator_functions import (
|
||||
base_scouter,
|
||||
build_tag_config,
|
||||
common_config,
|
||||
drift,
|
||||
gather_read_tags,
|
||||
minimal_retrain,
|
||||
overlap_filter_config,
|
||||
pi_web_api_scouter,
|
||||
predictions_batch,
|
||||
process_path_priority,
|
||||
scouter,
|
||||
simple_metrics,
|
||||
)
|
||||
|
||||
|
||||
def test_common_config():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
}
|
||||
result = common_config(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_common_config_preserves_explicit_runtime():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name'},
|
||||
'runtime': 'tenant-x',
|
||||
}
|
||||
assert common_config(config)['runtime'] == 'tenant-x'
|
||||
|
||||
|
||||
def test_drift():
|
||||
config = {
|
||||
'workflow_type': 'drift',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'interval_minutes': 120,
|
||||
'drift_metrics': ['kolmogorov_smirnov', 'jensen_shannon'],
|
||||
}
|
||||
result = drift(config)
|
||||
expected = {
|
||||
'workflow_type': 'drift',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'schema': 'sientia_data',
|
||||
'source_table_name': 'laborious_data',
|
||||
'target_table_name': 'drift_metrics',
|
||||
'interval': 120,
|
||||
'drift_metrics': ['kolmogorov_smirnov', 'jensen_shannon'],
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_simple_metrics():
|
||||
config = {
|
||||
'workflow_type': 'simple_metrics',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'interval_minutes': 120,
|
||||
'metrics': ['rmse', 'mse'],
|
||||
}
|
||||
result = simple_metrics(config)
|
||||
expected = {
|
||||
'workflow_type': 'simple_metrics',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'schema': 'sientia_data',
|
||||
'predictions_table_name': 'predictions',
|
||||
'data_table_name': 'laborious_data',
|
||||
'target_table_name': 'simple_metrics',
|
||||
'interval_minutes': 120,
|
||||
'metrics': ['rmse', 'mse'],
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_minimal_retrain():
|
||||
config = {
|
||||
'workflow_type': 'minimal_retrain',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'query': 'select * from sientia_data.laborious_data order by "timestamp" desc limit 30;',
|
||||
'datetime_columns': ['timestamp'],
|
||||
}
|
||||
result = minimal_retrain(config)
|
||||
expected = {
|
||||
'workflow_type': 'minimal_retrain',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'query': 'select * from sientia_data.laborious_data order by "timestamp" desc limit 30;',
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'log_retrain',
|
||||
'datetime_columns': ['timestamp'],
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_scouter():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'filters': [{'filter_name': 'test_filter_name', 'policy': 'test_policy'}],
|
||||
'read_tags': [
|
||||
{'tag_name': 'test_tag_name', 'aggr_func': 'test_aggr_func', 'data_range': [1, 2]}
|
||||
],
|
||||
'tag_retention_minutes': 10,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
result = scouter(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'topic': 'raw_test_schedule',
|
||||
'trigger_laborious': False,
|
||||
'filters': {'test_filter_name': {'policy': 'test_policy'}},
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'laborious_data',
|
||||
'retention_time': 10 * 60,
|
||||
'model_tags': {'test_tag_name': {'aggr_func': 'test_aggr_func', 'data_range': [1, 2]}},
|
||||
'debug_data_package': False,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'fill_missing_tags': False,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_overlap_filter_config():
|
||||
config = [
|
||||
{'filter_name': 'test_filter_name', 'policy': 'test_policy'},
|
||||
{'filter_name': 'test_filter_name2', 'policy': 'test_policy2'},
|
||||
]
|
||||
result = overlap_filter_config({'test_filter_name': {'policy': 'test_policy'}}, config)
|
||||
expected = {
|
||||
'test_filter_name': {'policy': 'test_policy', 'config': {}},
|
||||
'test_filter_name2': {'policy': 'test_policy2', 'config': {}},
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_process_path_priority():
|
||||
config = ['OTHER', 'STOP', 'CONTINUE']
|
||||
result = process_path_priority(config)
|
||||
expected = ['STOP', 'CONTINUE', 'REPEAT']
|
||||
assert result == expected
|
||||
|
||||
|
||||
@patch(
|
||||
'orchestrator.utils.orchestrator_functions.overlap_filter_config',
|
||||
return_value={'test_filter_name': {'policy': 'test_policy', 'config': {}}},
|
||||
)
|
||||
@patch(
|
||||
'orchestrator.utils.orchestrator_functions.process_path_priority',
|
||||
return_value=['STOP', 'CONTINUE', 'REPEAT'],
|
||||
)
|
||||
def test_predictions_batch(mock_process_path_priority, mock_overlap_filter_config):
|
||||
config = {
|
||||
'schedule_name': 'test_schedule',
|
||||
'workflow_type': 'predictions_batch',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'query': 'test_query',
|
||||
'write_tags': [
|
||||
{'server_id': 'test_server_id', 'type': 'prediction', 'addr': 'test_addr'},
|
||||
{'server_id': 'test_server_id', 'type': 'confidence', 'addr': 'test_addr'},
|
||||
],
|
||||
'pi_web_api_output_config': {
|
||||
'endpoint': 'test_endpoint',
|
||||
'prediction_tags': {
|
||||
'tag_1': 'webid_1',
|
||||
},
|
||||
'confidence_tags': {
|
||||
'tag_2': 'webid_2',
|
||||
},
|
||||
},
|
||||
'input_filters': [{'filter_name': 'test_filter_name', 'policy': 'test_policy'}],
|
||||
'mlflow_transform_filters': [{'filter_name': 'test_filter_name', 'policy': 'test_policy'}],
|
||||
'mlflow_predict_filters': [{'filter_name': 'test_filter_name', 'policy': 'test_policy'}],
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
'datetime_columns': ['timestamp'],
|
||||
'predictions_storage_policy': 'erl:1',
|
||||
}
|
||||
|
||||
result = predictions_batch(config)
|
||||
|
||||
mock_overlap_filter_config.assert_has_calls(
|
||||
[call({'EMPTY_DATA': {'policy': 'STOP', 'config': {}}}, config['input_filters'])]
|
||||
)
|
||||
mock_overlap_filter_config.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
{
|
||||
'EMPTY_DATA': {'policy': 'STOP', 'config': {}},
|
||||
'API_ERROR': {'policy': 'STOP', 'config': {}},
|
||||
},
|
||||
config['mlflow_transform_filters'],
|
||||
)
|
||||
]
|
||||
)
|
||||
mock_overlap_filter_config.assert_has_calls(
|
||||
[call({'API_ERROR': {'policy': 'STOP', 'config': {}}}, config['mlflow_predict_filters'])]
|
||||
)
|
||||
mock_process_path_priority.assert_called_once_with(config['path_priority'])
|
||||
|
||||
expected = {
|
||||
'workflow_type': 'predictions_batch',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'query': 'test_query',
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'predictions',
|
||||
'save_transform': True,
|
||||
'transform_table_name': 'transformed_data',
|
||||
'retention_time': 60 * 60,
|
||||
'opc_output_config': {
|
||||
'test_server_id': {
|
||||
'prediction_tags': {'test_addr': {'data_type': 'float'}},
|
||||
'confidence_tags': {'test_addr': {'data_type': 'float'}},
|
||||
}
|
||||
},
|
||||
'pi_web_api_output_config': {
|
||||
'endpoint': 'test_endpoint',
|
||||
'prediction_tags': {
|
||||
'tag_1': 'webid_1',
|
||||
},
|
||||
'confidence_tags': {
|
||||
'tag_2': 'webid_2',
|
||||
},
|
||||
},
|
||||
'input_filters': {'test_filter_name': {'policy': 'test_policy', 'config': {}}},
|
||||
'mlflow_transform_filters': {'test_filter_name': {'policy': 'test_policy', 'config': {}}},
|
||||
'mlflow_predict_filters': {'test_filter_name': {'policy': 'test_policy', 'config': {}}},
|
||||
'path_priority': ['STOP', 'CONTINUE', 'REPEAT'],
|
||||
'datetime_columns': ['timestamp'],
|
||||
'predictions_storage_policy': 'erl:1',
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_gather_read_tags():
|
||||
pipelines = [
|
||||
{
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule2',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address2',
|
||||
},
|
||||
{
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address3',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = gather_read_tags(pipelines)
|
||||
|
||||
expected = {
|
||||
'1:test_tag_address': {
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
'topics': ['raw_test_schedule'],
|
||||
},
|
||||
'2:test_tag_address2': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address2',
|
||||
'topics': ['raw_test_schedule2'],
|
||||
},
|
||||
'2:test_tag_address3': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address3',
|
||||
'topics': ['raw_test_schedule2'],
|
||||
},
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_gather_read_tags_filters_non_scouter_workflows():
|
||||
"""Test that gather_read_tags only processes scouter workflow types and ignores others"""
|
||||
pipelines = [
|
||||
{
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_scouter_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'predictions_batch',
|
||||
'schedule_name': 'test_predictions_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address_predictions',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'minimal_retrain',
|
||||
'schedule_name': 'test_retrain_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '3',
|
||||
'server_name': 'test_server_name3',
|
||||
'tag_address': 'test_tag_address_retrain',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'pi_web_api_scouter',
|
||||
'schedule_name': 'test_pi_web_api_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '4',
|
||||
'server_name': 'test_server_name4',
|
||||
'tag_address': 'test_tag_address_pi_web_api',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'drift',
|
||||
'schedule_name': 'test_drift_schedule',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '5',
|
||||
'server_name': 'test_server_name5',
|
||||
'tag_address': 'test_tag_address_drift',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_scouter_schedule2',
|
||||
'read_tags': [
|
||||
{
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address2',
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
result = gather_read_tags(pipelines)
|
||||
|
||||
# Only scouter workflow types should be included
|
||||
expected = {
|
||||
'1:test_tag_address': {
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
'topics': ['raw_test_scouter_schedule'],
|
||||
},
|
||||
'1:test_tag_address2': {
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address2',
|
||||
'topics': ['raw_test_scouter_schedule2'],
|
||||
},
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
# Ensure non-scouter pipelines are not included
|
||||
assert '2:test_tag_address_predictions' not in result
|
||||
assert '3:test_tag_address_retrain' not in result
|
||||
assert '4:test_tag_address_pi_web_api' not in result
|
||||
assert '5:test_tag_address_drift' not in result
|
||||
|
||||
|
||||
def test_build_tag_config():
|
||||
tags = [
|
||||
{
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
'frequency': 1000,
|
||||
},
|
||||
{
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address2',
|
||||
'frequency': 1000,
|
||||
},
|
||||
{
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address3',
|
||||
'frequency': 300,
|
||||
},
|
||||
{
|
||||
'server_id': '3',
|
||||
'server_name': 'test_server_name3',
|
||||
'tag_address': 'test_tag_address4',
|
||||
'frequency': 200,
|
||||
},
|
||||
]
|
||||
|
||||
opc_servers = {
|
||||
'1': {
|
||||
'server_name': 'test_server_name',
|
||||
'url': 'test_url',
|
||||
'uri': 'test_uri',
|
||||
},
|
||||
'2': {
|
||||
'server_name': 'test_server_name2',
|
||||
'url': 'test_url2',
|
||||
'uri': 'test_uri2',
|
||||
},
|
||||
}
|
||||
|
||||
result = build_tag_config(tags, opc_servers)
|
||||
|
||||
expected = {
|
||||
'test_server_name': {
|
||||
'server_id': '1',
|
||||
'name': 'test_server_name',
|
||||
'url': 'test_url',
|
||||
'server_uri': 'test_uri',
|
||||
'cert_path': None,
|
||||
'private_key_path': None,
|
||||
'server_cert_path': None,
|
||||
'subscription_period_ms': 500,
|
||||
'tags': {
|
||||
'test_tag_address': {
|
||||
'server_id': '1',
|
||||
'server_name': 'test_server_name',
|
||||
'tag_address': 'test_tag_address',
|
||||
'frequency': 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
'test_server_name2': {
|
||||
'server_id': '2',
|
||||
'name': 'test_server_name2',
|
||||
'url': 'test_url2',
|
||||
'server_uri': 'test_uri2',
|
||||
'cert_path': None,
|
||||
'private_key_path': None,
|
||||
'server_cert_path': None,
|
||||
'subscription_period_ms': 150,
|
||||
'tags': {
|
||||
'test_tag_address2': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address2',
|
||||
'frequency': 1000,
|
||||
},
|
||||
'test_tag_address3': {
|
||||
'server_id': '2',
|
||||
'server_name': 'test_server_name2',
|
||||
'tag_address': 'test_tag_address3',
|
||||
'frequency': 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert result == (expected, ['3'])
|
||||
|
||||
|
||||
def test_base_scouter():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'filters': [
|
||||
{'filter_name': 'test_filter_name', 'policy': 'test_policy'},
|
||||
{'filter_name': 'test_filter_name2', 'policy': 'test_policy2'},
|
||||
],
|
||||
'tag_retention_minutes': 30,
|
||||
'debug_data_package': True,
|
||||
'fill_missing_tags': True,
|
||||
}
|
||||
result = base_scouter(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'trigger_laborious': False,
|
||||
'filters': {
|
||||
'test_filter_name': {'policy': 'test_policy'},
|
||||
'test_filter_name2': {'policy': 'test_policy2'},
|
||||
},
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'laborious_data',
|
||||
'retention_time': 30 * 60,
|
||||
'debug_data_package': True,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'fill_missing_tags': True,
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_pi_web_api_scouter():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'filters': [{'filter_name': 'test_filter_name', 'policy': 'test_policy'}],
|
||||
'read_tags': {
|
||||
'test_tag_name': {
|
||||
'webid': 'test_webid',
|
||||
'aggr_func': 'test_aggr_func',
|
||||
'data_range': [1, 2],
|
||||
}
|
||||
},
|
||||
'tag_retention_minutes': 10,
|
||||
'pi_web_api_config': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
'period': '*-2d',
|
||||
'max_count': 5,
|
||||
'api_timeout': 30,
|
||||
},
|
||||
'frequency': '1m',
|
||||
}
|
||||
result = pi_web_api_scouter(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'trigger_laborious': False,
|
||||
'filters': {'test_filter_name': {'policy': 'test_policy'}},
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'laborious_data',
|
||||
'retention_time': 10 * 60,
|
||||
'model_tags': {
|
||||
'test_tag_name': {
|
||||
'webid': 'test_webid',
|
||||
'aggr_func': 'test_aggr_func',
|
||||
'data_range': [1, 2],
|
||||
}
|
||||
},
|
||||
'debug_data_package': False,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'fill_missing_tags': False,
|
||||
'pi_web_api_query': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
'period': '*-2d',
|
||||
'max_count': 5,
|
||||
'api_timeout': 30,
|
||||
},
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_pi_web_api_scouter_with_timeout_greater_than_frequency():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'filters': [],
|
||||
'read_tags': {
|
||||
'test_tag_name': {
|
||||
'webid': 'test_webid',
|
||||
}
|
||||
},
|
||||
'tag_retention_minutes': 10,
|
||||
'pi_web_api_config': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
'api_timeout': 120,
|
||||
},
|
||||
'frequency': '1m',
|
||||
}
|
||||
result = pi_web_api_scouter(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '1m',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'trigger_laborious': False,
|
||||
'filters': {},
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'laborious_data',
|
||||
'retention_time': 10 * 60,
|
||||
'model_tags': {
|
||||
'test_tag_name': {'webid': 'test_webid', 'aggr_func': 'lts', 'data_range': [-100, 100]}
|
||||
},
|
||||
'debug_data_package': False,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'fill_missing_tags': False,
|
||||
'pi_web_api_query': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
'period': '*-1d',
|
||||
'max_count': 1,
|
||||
'api_timeout': 60,
|
||||
},
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_pi_web_api_scouter_with_no_timeout():
|
||||
config = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'model_id': 'test_model_id',
|
||||
'model': {'name': 'test_model_name', 'model_config': {'test_config': 'test_config'}},
|
||||
'filters': [],
|
||||
'read_tags': {
|
||||
'test_tag_name': {
|
||||
'webid': 'test_webid',
|
||||
}
|
||||
},
|
||||
'tag_retention_minutes': 10,
|
||||
'pi_web_api_config': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
},
|
||||
'frequency': '30s',
|
||||
}
|
||||
result = pi_web_api_scouter(config)
|
||||
expected = {
|
||||
'workflow_type': 'scouter',
|
||||
'schedule_name': 'test_schedule',
|
||||
'frequency': '30s',
|
||||
'offset': '0m',
|
||||
'max_retry_policy': 1,
|
||||
'model_id': 'test_model_id',
|
||||
'model_name': 'test_model_name',
|
||||
'model_config': {'test_config': 'test_config'},
|
||||
'on_conflict': 'error',
|
||||
'trigger_laborious': False,
|
||||
'filters': {},
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'laborious_data',
|
||||
'retention_time': 10 * 60,
|
||||
'model_tags': {
|
||||
'test_tag_name': {'webid': 'test_webid', 'aggr_func': 'lts', 'data_range': [-100, 100]}
|
||||
},
|
||||
'debug_data_package': False,
|
||||
'execution_timeout_seconds': 300,
|
||||
'task_timeout_seconds': 300,
|
||||
'fill_missing_tags': False,
|
||||
'pi_web_api_query': {
|
||||
'endpoint': 'https://test-endpoint.com',
|
||||
'period': '*-1d',
|
||||
'max_count': 1,
|
||||
'api_timeout': 30,
|
||||
},
|
||||
'runtime': 'legacy',
|
||||
}
|
||||
assert result == expected
|
||||
49
tests/orchestrator/worker/test_worker.py
Normal file
49
tests/orchestrator/worker/test_worker.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from orchestrator.worker.worker import main
|
||||
from orchestrator.workflows.alerts import Alerts
|
||||
from orchestrator.workflows.orchestrator import Orchestrator
|
||||
from orchestrator.workflows.reports import Reports
|
||||
|
||||
|
||||
def test_main_is_coroutine():
|
||||
assert asyncio.iscoroutinefunction(main)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.worker.worker.sys')
|
||||
@patch('orchestrator.worker.worker.start_http_server')
|
||||
@patch('orchestrator.worker.worker.NotificationHandler')
|
||||
@patch('orchestrator.worker.worker.Activities')
|
||||
@patch('orchestrator.worker.worker.prepare_worker')
|
||||
@patch('orchestrator.worker.worker.client.Client.connect', new_callable=AsyncMock)
|
||||
async def test_main_starts_three_workers_for_expected_workflows(
|
||||
_connect_mock,
|
||||
prepare_worker_mock,
|
||||
activities_mock,
|
||||
_notification_handler_mock,
|
||||
_start_http_server,
|
||||
_sys_mock,
|
||||
):
|
||||
"""
|
||||
main() must spin up exactly three Temporal workers, one per main workflow
|
||||
(Orchestrator, Alerts, Reports), and call run() on each.
|
||||
"""
|
||||
activities_instance = activities_mock.return_value
|
||||
activities_instance.connect_to_temporal = AsyncMock()
|
||||
|
||||
worker_mock = MagicMock()
|
||||
worker_mock.run = AsyncMock()
|
||||
prepare_worker_mock.return_value = worker_mock
|
||||
|
||||
await main()
|
||||
|
||||
assert prepare_worker_mock.call_count == 3
|
||||
|
||||
main_workflows = [call.kwargs['main_workflow'] for call in prepare_worker_mock.call_args_list]
|
||||
assert main_workflows == [Orchestrator, Alerts, Reports]
|
||||
|
||||
assert worker_mock.run.call_count == 3
|
||||
0
tests/orchestrator/workflows/__init__.py
Normal file
0
tests/orchestrator/workflows/__init__.py
Normal file
@@ -0,0 +1,171 @@
|
||||
from unittest.mock import ANY, AsyncMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.workflows.subworkflows.load_notification_package import LoadNotificationPackage
|
||||
|
||||
|
||||
@fixture
|
||||
def load_notification_package():
|
||||
return LoadNotificationPackage()
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test-schedule-name',
|
||||
'workflow_name': 'test-workflow',
|
||||
'model_name': '-',
|
||||
'model_id': '-',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'orchestrator.workflows.subworkflows.load_notification_package.workflow', new_callable=AsyncMock
|
||||
)
|
||||
async def test_run(workflow_mock, load_notification_package):
|
||||
input_data = {
|
||||
'metadata': metadata,
|
||||
'mail_type': 'test_mail_type',
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
}
|
||||
|
||||
workflow_mock.start_local_activity_method.side_effect = [
|
||||
'2023-01-01 12:00:00',
|
||||
[
|
||||
{
|
||||
'id': '1',
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
'id_r': '1',
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
output = await load_notification_package.run(input_data)
|
||||
|
||||
assert output == {
|
||||
'last_timestamp': '2023-01-01 12:00:00',
|
||||
'notification_package': [
|
||||
{
|
||||
'id': '1',
|
||||
}
|
||||
],
|
||||
'sending_configs': [
|
||||
{
|
||||
'id_r': '1',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.get_last_data_timestamp,
|
||||
{**input_data['metadata'], 'mail_type': 'test_mail_type'},
|
||||
start_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.find_documents_in_mongodb,
|
||||
{
|
||||
**input_data['metadata'],
|
||||
'query': {'collection': 'receiver_groups', 'filters': {'active': True}},
|
||||
},
|
||||
start_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.load_latest_data,
|
||||
{
|
||||
**input_data['metadata'],
|
||||
'collection_name': 'notification_queue',
|
||||
'last_data_timestamp': '2023-01-01 12:00:00',
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
},
|
||||
start_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.put_last_data_timestamp,
|
||||
{
|
||||
**input_data['metadata'],
|
||||
'data': [
|
||||
{
|
||||
'id': '1',
|
||||
}
|
||||
],
|
||||
'mail_type': 'test_mail_type',
|
||||
},
|
||||
start_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'orchestrator.workflows.subworkflows.load_notification_package.workflow', new_callable=AsyncMock
|
||||
)
|
||||
async def test_run_no_data(workflow_mock, load_notification_package):
|
||||
input_data = {
|
||||
'metadata': metadata,
|
||||
'mail_type': 'test_mail_type',
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
}
|
||||
|
||||
workflow_mock.start_local_activity_method.side_effect = ['2023-01-01 12:00:00', [], []]
|
||||
|
||||
output = await load_notification_package.run(input_data)
|
||||
|
||||
assert output == {
|
||||
'last_timestamp': '2023-01-01 12:00:00',
|
||||
'notification_package': [],
|
||||
'sending_configs': [],
|
||||
}
|
||||
|
||||
workflow_mock.start_activity_method.assert_not_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch(
|
||||
'orchestrator.workflows.subworkflows.load_notification_package.workflow', new_callable=AsyncMock
|
||||
)
|
||||
async def test_run_has_data(workflow_mock, load_notification_package):
|
||||
input_data = {
|
||||
'metadata': metadata,
|
||||
'base_data_filter': {'level': 'ERROR'},
|
||||
'mail_type': 'test_mail_type',
|
||||
}
|
||||
|
||||
workflow_mock.start_local_activity_method.side_effect = ['2023-01-01 12:00:00', ['data'], []]
|
||||
|
||||
output = await load_notification_package.run(input_data)
|
||||
|
||||
assert output == {
|
||||
'last_timestamp': '2023-01-01 12:00:00',
|
||||
'notification_package': ['data'],
|
||||
'sending_configs': [],
|
||||
}
|
||||
|
||||
workflow_mock.start_activity_method.assert_not_called()
|
||||
@@ -0,0 +1,151 @@
|
||||
from unittest.mock import ANY, AsyncMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
from sientia_do.temporal.constants import DATETIME_FORMAT_WITH_TZ
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.workflows.subworkflows.process_notifications import ProcessNotifications
|
||||
|
||||
|
||||
@fixture
|
||||
def process_notifications():
|
||||
return ProcessNotifications()
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test-schedule-name',
|
||||
'workflow_name': 'test-workflow',
|
||||
'model_name': '-',
|
||||
'model_id': '-',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.subworkflows.process_notifications.workflow', new_callable=AsyncMock)
|
||||
async def test_run(workflow_mock, process_notifications):
|
||||
input_data = {
|
||||
'metadata': metadata,
|
||||
'notification_package': ['content'],
|
||||
'mail_type': 'test_mail_type',
|
||||
'schema': 'test_schema',
|
||||
'table_name': 'test_table_name',
|
||||
}
|
||||
|
||||
response = await process_notifications.run(input_data)
|
||||
|
||||
assert response == workflow_mock.execute_local_activity_method.return_value
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.build_email_html,
|
||||
{
|
||||
**metadata,
|
||||
'receiver_groups': input_data['notification_package'],
|
||||
'mail_type': input_data['mail_type'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
workflow_mock.execute_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.format_log_report,
|
||||
{
|
||||
**metadata,
|
||||
'receiver_groups': workflow_mock.execute_activity_method.return_value,
|
||||
'mail_type': input_data['mail_type'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.send_email,
|
||||
{
|
||||
**metadata,
|
||||
'receiver_groups': workflow_mock.execute_local_activity_method.return_value,
|
||||
'mail_type': input_data['mail_type'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.export_data_to_postgres,
|
||||
{
|
||||
**metadata,
|
||||
'schema': input_data['schema'],
|
||||
'table_name': input_data['table_name'],
|
||||
'data': workflow_mock.execute_local_activity_method.return_value,
|
||||
'timestamp_conversion': {
|
||||
'column': 'timestamp',
|
||||
'format': DATETIME_FORMAT_WITH_TZ,
|
||||
},
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.subworkflows.process_notifications.workflow', new_callable=AsyncMock)
|
||||
async def test_run_send_email_return_empty(workflow_mock, process_notifications):
|
||||
input_data = {
|
||||
'metadata': metadata,
|
||||
'notification_package': ['content'],
|
||||
'mail_type': 'test_mail_type',
|
||||
'schema': 'test_schema',
|
||||
'table_name': 'test_table_name',
|
||||
}
|
||||
|
||||
workflow_mock.execute_activity_method.return_value = []
|
||||
|
||||
await process_notifications.run(input_data)
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.build_email_html,
|
||||
{
|
||||
**metadata,
|
||||
'receiver_groups': input_data['notification_package'],
|
||||
'mail_type': input_data['mail_type'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.send_email,
|
||||
{
|
||||
**metadata,
|
||||
'receiver_groups': workflow_mock.execute_local_activity_method.return_value,
|
||||
'mail_type': input_data['mail_type'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert workflow_mock.execute_activity_method.call_count == 1
|
||||
assert workflow_mock.execute_local_activity_method.call_count == 1
|
||||
137
tests/orchestrator/workflows/test_alerts.py
Normal file
137
tests/orchestrator/workflows/test_alerts.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.workflows.alerts import Alerts
|
||||
|
||||
|
||||
@fixture
|
||||
def alerts():
|
||||
return Alerts()
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test-schedule-name',
|
||||
'workflow_name': 'alerts',
|
||||
'model_name': '-',
|
||||
'model_id': '-',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.alerts.workflow', new_callable=AsyncMock)
|
||||
async def test_run_full_flow(workflow_mock, alerts):
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await alerts.run(input_data)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.load_notification_package',
|
||||
{**input_data, 'metadata': metadata, 'base_data_filter': {'level': 'ERROR'}},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.process_notifications',
|
||||
{
|
||||
'metadata': metadata,
|
||||
'mail_type': 'Alerts',
|
||||
'notification_package': workflow_mock.execute_local_activity_method.return_value,
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'log_report',
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.filter_notification_alerts,
|
||||
{
|
||||
**metadata,
|
||||
'notification_package': workflow_mock.execute_child_workflow.return_value[
|
||||
'notification_package'
|
||||
],
|
||||
'sending_configs': workflow_mock.execute_child_workflow.return_value[
|
||||
'sending_configs'
|
||||
],
|
||||
'notification_ttl': input_data['notification_ttl'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.store_notification_cache,
|
||||
{
|
||||
**metadata,
|
||||
'log_report': workflow_mock.execute_child_workflow.return_value,
|
||||
'sent_ttl': input_data['sent_ttl'],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.alerts.workflow', new_callable=AsyncMock)
|
||||
async def test_run_no_data(workflow_mock, alerts):
|
||||
workflow_mock.execute_child_workflow.return_value = {
|
||||
'last_timestamp': '2023-01-01 12:00:00.000000',
|
||||
'notification_package': [],
|
||||
'sending_configs': [],
|
||||
}
|
||||
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await alerts.run(input_data)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.load_notification_package',
|
||||
{**input_data, 'metadata': metadata, 'base_data_filter': {'level': 'ERROR'}},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_not_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.alerts.workflow', new_callable=AsyncMock)
|
||||
async def test_run_no_receiver_groups(workflow_mock, alerts):
|
||||
workflow_mock.execute_local_activity_method.return_value = {}
|
||||
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await alerts.run(input_data)
|
||||
|
||||
assert workflow_mock.execute_child_workflow.call_count == 1
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.alerts.workflow', new_callable=AsyncMock)
|
||||
async def test_run_no_log_report(workflow_mock, alerts):
|
||||
workflow_mock.execute_child_workflow.side_effect = [MagicMock(), []]
|
||||
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await alerts.run(input_data)
|
||||
|
||||
workflow_mock.execute_activity_method.assert_not_called()
|
||||
348
tests/orchestrator/workflows/test_orchestrator.py
Normal file
348
tests/orchestrator/workflows/test_orchestrator.py
Normal file
@@ -0,0 +1,348 @@
|
||||
from unittest.mock import ANY, AsyncMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.workflows.orchestrator import Orchestrator
|
||||
|
||||
|
||||
@fixture
|
||||
def orchestrator():
|
||||
return Orchestrator()
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test-schedule-name',
|
||||
'workflow_name': 'orchestrator',
|
||||
'model_name': '-',
|
||||
'model_id': '-',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.orchestrator.workflow', new_callable=AsyncMock)
|
||||
async def test_run(workflow_mock, orchestrator):
|
||||
input_data = {
|
||||
'pipelines_query': 'SELECT * FROM bucket',
|
||||
'opc_servers_query': 'SELECT * FROM servers',
|
||||
'schedule_name': 'test-schedule-name',
|
||||
}
|
||||
|
||||
await orchestrator.run(input_data)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.aggregate_documents_in_mongodb,
|
||||
{
|
||||
**metadata,
|
||||
'query': input_data['pipelines_query'],
|
||||
'timestamp_fields': ['updated_at'],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.find_documents_in_mongodb,
|
||||
{**metadata, 'query': input_data['opc_servers_query']},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.find_documents_in_mongodb,
|
||||
{
|
||||
**metadata,
|
||||
'query': {'collection': 'orchestrated_schedules'},
|
||||
'timestamp_fields': ['updated_at'],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.load_opc_slots,
|
||||
{**metadata},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.load_active_ingestors,
|
||||
{**metadata},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.format_schedule_config,
|
||||
{
|
||||
**metadata,
|
||||
'schedule_config': workflow_mock.start_local_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.process_schedules,
|
||||
{**metadata, 'pipelines': workflow_mock.start_local_activity_method.return_value},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.process_slots,
|
||||
{
|
||||
**metadata,
|
||||
'opc_servers': workflow_mock.start_local_activity_method.return_value,
|
||||
'active_ingestors': workflow_mock.start_local_activity_method.return_value,
|
||||
'pipelines': workflow_mock.start_local_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.create_schedule_config,
|
||||
{
|
||||
**metadata,
|
||||
'current_schedule_config': workflow_mock.start_local_activity_method.return_value,
|
||||
'schedule_config': workflow_mock.start_local_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.create_slot_config,
|
||||
{
|
||||
**metadata,
|
||||
'current_slot_config': workflow_mock.start_local_activity_method.return_value,
|
||||
'slot_config': workflow_mock.start_local_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.normalize_schedules,
|
||||
{
|
||||
**metadata,
|
||||
'orchestrated_schedules': workflow_mock.start_local_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.create_collection_with_ttl_index,
|
||||
{
|
||||
**metadata,
|
||||
'pipelines': workflow_mock.start_local_activity_method.return_value['scouter'],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.delete_slots,
|
||||
{
|
||||
**metadata,
|
||||
'to_delete': workflow_mock.start_local_activity_method.return_value[
|
||||
'to_delete'
|
||||
],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.update_slots,
|
||||
{
|
||||
**metadata,
|
||||
'to_insert': workflow_mock.start_local_activity_method.return_value[
|
||||
'to_insert'
|
||||
],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.delete_schedules,
|
||||
{
|
||||
**metadata,
|
||||
'schedules': workflow_mock.start_local_activity_method.return_value[
|
||||
'to_delete'
|
||||
],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.create_schedules,
|
||||
{
|
||||
**metadata,
|
||||
'schedules': workflow_mock.start_local_activity_method.return_value[
|
||||
'to_create'
|
||||
],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.update_schedules,
|
||||
{
|
||||
**metadata,
|
||||
'schedules': workflow_mock.start_local_activity_method.return_value[
|
||||
'to_update'
|
||||
],
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.report_schedule_orchestration,
|
||||
{
|
||||
**metadata,
|
||||
'created_schedules': workflow_mock.start_activity_method.return_value,
|
||||
'updated_schedules': workflow_mock.start_activity_method.return_value,
|
||||
'deleted_schedules': workflow_mock.start_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.report_slot_orchestration,
|
||||
{
|
||||
**metadata,
|
||||
'inserted_slots': workflow_mock.start_activity_method.return_value,
|
||||
'deleted_slots': workflow_mock.start_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.update_pipelines_timestamps,
|
||||
{
|
||||
**metadata,
|
||||
'updated_pipelines': workflow_mock.start_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.delete_pipelines_timestamps,
|
||||
{
|
||||
**metadata,
|
||||
'deleted_pipelines': workflow_mock.start_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.start_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.create_pipelines_timestamps,
|
||||
{
|
||||
**metadata,
|
||||
'created_pipelines': workflow_mock.start_activity_method.return_value,
|
||||
},
|
||||
retry_policy=ANY,
|
||||
start_to_close_timeout=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
109
tests/orchestrator/workflows/test_reports.py
Normal file
109
tests/orchestrator/workflows/test_reports.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from unittest.mock import ANY, AsyncMock, call, patch
|
||||
|
||||
from pytest import fixture, mark
|
||||
|
||||
from orchestrator.activities.activities import Activities
|
||||
from orchestrator.workflows.reports import Reports
|
||||
|
||||
|
||||
@fixture
|
||||
def reports():
|
||||
return Reports()
|
||||
|
||||
|
||||
metadata = {
|
||||
'metadata': {
|
||||
'schedule_name': 'test-schedule-name',
|
||||
'workflow_name': 'reports',
|
||||
'model_name': '-',
|
||||
'model_id': '-',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.reports.workflow', new_callable=AsyncMock)
|
||||
async def test_run_full_flow(workflow_mock, reports):
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await reports.run(input_data)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.load_notification_package',
|
||||
{**input_data, 'metadata': metadata, 'base_data_filter': {}},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.process_notifications',
|
||||
{
|
||||
'metadata': metadata,
|
||||
'mail_type': 'Reports',
|
||||
'notification_package': workflow_mock.execute_local_activity_method.return_value,
|
||||
'schema': 'sientia_data',
|
||||
'table_name': 'log_report',
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
Activities.filter_notification_reports,
|
||||
{
|
||||
**metadata,
|
||||
'notification_package': workflow_mock.execute_child_workflow.return_value[
|
||||
'notification_package'
|
||||
],
|
||||
'sending_configs': workflow_mock.execute_child_workflow.return_value[
|
||||
'sending_configs'
|
||||
],
|
||||
},
|
||||
schedule_to_close_timeout=ANY,
|
||||
retry_policy=ANY,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.reports.workflow', new_callable=AsyncMock)
|
||||
async def test_run_no_data(workflow_mock, reports):
|
||||
workflow_mock.execute_child_workflow.return_value = {
|
||||
'last_timestamp': '2023-01-01 12:00:00.000000',
|
||||
'notification_package': [],
|
||||
'sending_configs': [],
|
||||
}
|
||||
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await reports.run(input_data)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'subworkflow.load_notification_package',
|
||||
{**input_data, 'metadata': metadata, 'base_data_filter': {}},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
workflow_mock.execute_local_activity_method.assert_not_called()
|
||||
|
||||
|
||||
@mark.asyncio
|
||||
@patch('orchestrator.workflows.reports.workflow', new_callable=AsyncMock)
|
||||
async def test_run_no_groups(workflow_mock, reports):
|
||||
workflow_mock.execute_local_activity_method.return_value = []
|
||||
|
||||
input_data = {'schedule_name': 'test-schedule-name', 'notification_ttl': 300, 'sent_ttl': 600}
|
||||
|
||||
await reports.run(input_data)
|
||||
|
||||
workflow_mock.execute_child_workflow.assert_called_once()
|
||||
Reference in New Issue
Block a user