QTZPOC-20
QTZPOC-20: Enhance system robustness and test coverage across bundle processing, activity error handling, and worker operations. * Strengthen bundle reader validation with new tests for archive integrity, security vulnerabilities (e.g., path traversal, duplicate entries, special file types), and I/O error resilience. * Improve error handling and status propagation in PI Web API, MLflow, and OPC activities, including partial write scenarios and OPC session management. * Add comprehensive tests for worker startup, configuration, graceful shutdown, and Prometheus metrics server initialization. * Introduce a new utility for intelligent DataFrame debugging, summarizing large dataframes for concise logging, and refine exception chain summarization. * Update code coverage configuration to exclude @overload decorator lines.
This commit is contained in:
245
tests/laborious/worker/test_worker_main.py
Normal file
245
tests/laborious/worker/test_worker_main.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""``main()`` and ``start_prometheus_server()``: the orchestration paths ``test_runtime_task_queues``
|
||||
doesn't reach.
|
||||
|
||||
Everything I/O-bound (Temporal client, Prometheus HTTP server, Mongo/Postgres/MinIO configs) is
|
||||
mocked; only the control flow inside ``worker.py`` is under test.
|
||||
"""
|
||||
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||
|
||||
from pytest import mark, raises
|
||||
|
||||
from laborious.worker.worker import main, start_prometheus_server
|
||||
|
||||
|
||||
@patch('laborious.worker.worker.metrics.APP_UP')
|
||||
@patch('laborious.worker.worker.get_logger')
|
||||
def test_main_exits_when_runtime_is_unset(mock_get_logger, mock_app_up, monkeypatch):
|
||||
monkeypatch.delenv('RUNTIME', raising=False)
|
||||
logger = MagicMock()
|
||||
mock_get_logger.return_value = logger
|
||||
|
||||
with raises(SystemExit) as exc:
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
assert exc.value.code == 1
|
||||
logger.custom_critical.assert_called_once()
|
||||
mock_app_up.labels.return_value.set.assert_called_once_with(0)
|
||||
|
||||
|
||||
@patch('laborious.worker.worker.prepare_worker')
|
||||
@patch('laborious.worker.worker.client.Client.connect', new_callable=AsyncMock)
|
||||
@patch('laborious.worker.worker.TelemetryConfig')
|
||||
@patch('laborious.worker.worker.PrometheusConfig')
|
||||
@patch('laborious.worker.worker.Runtime')
|
||||
@patch('laborious.worker.worker.Activities')
|
||||
@patch('laborious.worker.worker.NotificationHandler')
|
||||
@patch('laborious.worker.worker.build_import_status_config')
|
||||
@patch('laborious.worker.worker.build_import_config')
|
||||
@patch('laborious.worker.worker.build_api_config')
|
||||
@patch('laborious.worker.worker.build_opc_config')
|
||||
@patch('laborious.worker.worker.build_minio_config')
|
||||
@patch('laborious.worker.worker.build_mlflow_config')
|
||||
@patch('laborious.worker.worker.build_postgres_config')
|
||||
@patch('laborious.worker.worker.build_mongodb_config')
|
||||
@patch('laborious.worker.worker.start_prometheus_server')
|
||||
@patch('laborious.worker.worker.get_logger')
|
||||
class TestMainHappyPath:
|
||||
"""One class so every test shares the same (long) patch stack."""
|
||||
|
||||
def _run(self, mock_prepare_worker, mock_connect):
|
||||
worker = MagicMock()
|
||||
worker.run = AsyncMock(return_value=None)
|
||||
mock_prepare_worker.return_value = worker
|
||||
mock_connect.return_value = MagicMock()
|
||||
|
||||
import asyncio
|
||||
|
||||
with raises(SystemExit) as exc:
|
||||
asyncio.run(main())
|
||||
return exc.value.code
|
||||
|
||||
def test_runs_four_workers_without_the_import_flag( # noqa: PLR0913
|
||||
self,
|
||||
mock_get_logger,
|
||||
_mock_start_prometheus,
|
||||
_mock_mongo_cfg,
|
||||
_mock_pg_cfg,
|
||||
_mock_mlflow_cfg,
|
||||
_mock_minio_cfg,
|
||||
_mock_opc_cfg,
|
||||
_mock_api_cfg,
|
||||
_mock_import_cfg,
|
||||
_mock_import_status_cfg,
|
||||
mock_notification_handler,
|
||||
mock_activities,
|
||||
_mock_runtime,
|
||||
_mock_prometheus_config,
|
||||
_mock_telemetry_config,
|
||||
mock_connect,
|
||||
mock_prepare_worker,
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv('RUNTIME', 'test-runtime')
|
||||
monkeypatch.delenv('IMPORT_MODEL_ENABLED', raising=False)
|
||||
mock_get_logger.return_value = MagicMock()
|
||||
mock_activities.return_value.init_opc = AsyncMock()
|
||||
mock_activities.return_value.shutdown = AsyncMock()
|
||||
|
||||
exit_code = self._run(mock_prepare_worker, mock_connect)
|
||||
|
||||
assert exit_code == 0
|
||||
assert mock_prepare_worker.call_count == 4
|
||||
mock_activities.return_value.init_opc.assert_awaited_once()
|
||||
mock_activities.return_value.shutdown.assert_awaited_once()
|
||||
mock_notification_handler.return_value.shutdown.assert_called_once()
|
||||
|
||||
def test_runs_five_workers_with_the_import_flag( # noqa: PLR0913
|
||||
self,
|
||||
mock_get_logger,
|
||||
_mock_start_prometheus,
|
||||
_mock_mongo_cfg,
|
||||
_mock_pg_cfg,
|
||||
_mock_mlflow_cfg,
|
||||
_mock_minio_cfg,
|
||||
_mock_opc_cfg,
|
||||
_mock_api_cfg,
|
||||
_mock_import_cfg,
|
||||
_mock_import_status_cfg,
|
||||
_mock_notification_handler,
|
||||
mock_activities,
|
||||
_mock_runtime,
|
||||
_mock_prometheus_config,
|
||||
_mock_telemetry_config,
|
||||
mock_connect,
|
||||
mock_prepare_worker,
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv('RUNTIME', 'test-runtime')
|
||||
monkeypatch.setenv('IMPORT_MODEL_ENABLED', 'true')
|
||||
mock_get_logger.return_value = MagicMock()
|
||||
mock_activities.return_value.init_opc = AsyncMock()
|
||||
mock_activities.return_value.shutdown = AsyncMock()
|
||||
|
||||
exit_code = self._run(mock_prepare_worker, mock_connect)
|
||||
|
||||
assert exit_code == 0
|
||||
assert mock_prepare_worker.call_count == 5
|
||||
|
||||
def test_a_falsy_notification_handler_or_activities_skips_shutdown( # noqa: PLR0913
|
||||
self,
|
||||
mock_get_logger,
|
||||
_mock_start_prometheus,
|
||||
_mock_mongo_cfg,
|
||||
_mock_pg_cfg,
|
||||
_mock_mlflow_cfg,
|
||||
_mock_minio_cfg,
|
||||
_mock_opc_cfg,
|
||||
_mock_api_cfg,
|
||||
_mock_import_cfg,
|
||||
_mock_import_status_cfg,
|
||||
mock_notification_handler,
|
||||
mock_activities,
|
||||
_mock_runtime,
|
||||
_mock_prometheus_config,
|
||||
_mock_telemetry_config,
|
||||
mock_connect,
|
||||
mock_prepare_worker,
|
||||
monkeypatch,
|
||||
):
|
||||
"""Defensive `if notification_handler:` / `if activities:` guards around shutdown — real
|
||||
construction always yields truthy instances, but the guard itself still needs a test."""
|
||||
monkeypatch.setenv('RUNTIME', 'test-runtime')
|
||||
monkeypatch.delenv('IMPORT_MODEL_ENABLED', raising=False)
|
||||
mock_get_logger.return_value = MagicMock()
|
||||
mock_activities.return_value.init_opc = AsyncMock()
|
||||
mock_activities.return_value.shutdown = AsyncMock()
|
||||
mock_activities.return_value.__bool__.return_value = False
|
||||
mock_notification_handler.return_value.__bool__.return_value = False
|
||||
|
||||
exit_code = self._run(mock_prepare_worker, mock_connect)
|
||||
|
||||
assert exit_code == 0
|
||||
mock_activities.return_value.shutdown.assert_not_called()
|
||||
mock_notification_handler.return_value.shutdown.assert_not_called()
|
||||
|
||||
def test_a_worker_failure_exits_non_zero_and_still_cleans_up( # noqa: PLR0913
|
||||
self,
|
||||
mock_get_logger,
|
||||
_mock_start_prometheus,
|
||||
_mock_mongo_cfg,
|
||||
_mock_pg_cfg,
|
||||
_mock_mlflow_cfg,
|
||||
_mock_minio_cfg,
|
||||
_mock_opc_cfg,
|
||||
_mock_api_cfg,
|
||||
_mock_import_cfg,
|
||||
_mock_import_status_cfg,
|
||||
mock_notification_handler,
|
||||
mock_activities,
|
||||
_mock_runtime,
|
||||
_mock_prometheus_config,
|
||||
_mock_telemetry_config,
|
||||
mock_connect,
|
||||
mock_prepare_worker,
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv('RUNTIME', 'test-runtime')
|
||||
monkeypatch.delenv('IMPORT_MODEL_ENABLED', raising=False)
|
||||
logger = MagicMock()
|
||||
mock_get_logger.return_value = logger
|
||||
mock_activities.return_value.init_opc = AsyncMock()
|
||||
mock_activities.return_value.shutdown = AsyncMock()
|
||||
mock_connect.return_value = MagicMock()
|
||||
|
||||
worker = MagicMock()
|
||||
worker.run = AsyncMock(side_effect=RuntimeError('boom'))
|
||||
mock_prepare_worker.return_value = worker
|
||||
|
||||
import asyncio
|
||||
|
||||
with raises(SystemExit) as exc:
|
||||
asyncio.run(main())
|
||||
|
||||
assert exc.value.code == 1
|
||||
logger.custom_error.assert_called_once_with(ANY, ANY)
|
||||
mock_activities.return_value.shutdown.assert_awaited_once()
|
||||
mock_notification_handler.return_value.shutdown.assert_called_once()
|
||||
|
||||
|
||||
@patch('laborious.worker.worker.metrics.APP_UP')
|
||||
@patch('laborious.worker.worker.start_http_server')
|
||||
def test_start_prometheus_server_marks_the_app_up(mock_start_http_server, mock_app_up):
|
||||
start_prometheus_server()
|
||||
|
||||
mock_start_http_server.assert_called_once()
|
||||
mock_app_up.labels.return_value.set.assert_called_once_with(1)
|
||||
|
||||
|
||||
@patch('laborious.worker.worker.os._exit')
|
||||
@patch('laborious.worker.worker.metrics.APP_UP')
|
||||
@patch('laborious.worker.worker.start_http_server')
|
||||
def test_start_prometheus_server_exits_the_process_on_failure(
|
||||
mock_start_http_server, mock_app_up, mock_os_exit
|
||||
):
|
||||
mock_start_http_server.side_effect = OSError('port in use')
|
||||
|
||||
start_prometheus_server()
|
||||
|
||||
mock_os_exit.assert_called_once_with(1)
|
||||
mock_app_up.labels.return_value.set.assert_not_called()
|
||||
|
||||
|
||||
@mark.parametrize('port_env', ['9999'])
|
||||
@patch('laborious.worker.worker.metrics.APP_UP')
|
||||
@patch('laborious.worker.worker.start_http_server')
|
||||
def test_start_prometheus_server_reads_the_configured_port(
|
||||
mock_start_http_server, _mock_app_up, port_env, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv('HTTP_METRICS_PORT', port_env)
|
||||
|
||||
start_prometheus_server()
|
||||
|
||||
mock_start_http_server.assert_called_once_with(int(port_env))
|
||||
Reference in New Issue
Block a user