feat(tests): fixtures for end-2-end tests (available as a pytest plugin)

This commit is contained in:
guijar_m 2024-03-06 10:12:45 +01:00
parent b5ca2c30dd
commit b24f65a2a1
6 changed files with 353 additions and 221 deletions

View File

@ -7,41 +7,18 @@ from unittest.mock import PropertyMock
import numpy as np import numpy as np
import pytest import pytest
from bec_client import BECIPythonClient
from bec_client.callbacks.utils import ScanRequestError from bec_client.callbacks.utils import ScanRequestError
from bec_lib import MessageEndpoints, RedisConnector, ServiceConfig, bec_logger, configs from bec_lib import MessageEndpoints, bec_logger, configs
from bec_lib.alarm_handler import AlarmBase from bec_lib.alarm_handler import AlarmBase
from bec_lib.bec_errors import ScanAbortion, ScanInterruption from bec_lib.bec_errors import ScanAbortion, ScanInterruption
from bec_lib.tests.utils import wait_for_empty_queue
logger = bec_logger.logger logger = bec_logger.logger
CONFIG_PATH = "../ci/test_config.yaml"
# CONFIG_PATH = "../bec_config_dev.yaml"
# pylint: disable=no-member
# pylint: disable=missing-function-docstring
# pylint: disable=redefined-outer-name
# pylint: disable=protected-access
# pylint: disable=undefined-variable
@pytest.fixture(scope="function")
def client():
config = ServiceConfig(CONFIG_PATH)
bec = BECIPythonClient(config, RedisConnector, forced=True)
bec.start()
bec.queue.request_queue_reset()
bec.queue.request_scan_continuation()
time.sleep(1)
yield bec
bec.shutdown()
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_grid_scan(capsys, client): def test_grid_scan(capsys, bec_client_fixture):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_grid_scan"}) bec.metadata.update({"unit_test": "test_grid_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans.umv(dev.samx, 0, dev.samy, 0, relative=False) scans.umv(dev.samx, 0, dev.samy, 0, relative=False)
@ -53,10 +30,9 @@ def test_grid_scan(capsys, client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_fermat_scan(capsys, client): def test_fermat_scan(capsys, bec_client_fixture):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_fermat_scan"}) bec.metadata.update({"unit_test": "test_fermat_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
status = scans.fermat_scan( status = scans.fermat_scan(
@ -78,10 +54,9 @@ def test_fermat_scan(capsys, client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_line_scan(capsys, client): def test_line_scan(capsys, bec_client_fixture):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_line_scan"}) bec.metadata.update({"unit_test": "test_line_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.01, relative=True) status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.01, relative=True)
@ -93,10 +68,9 @@ def test_line_scan(capsys, client):
@pytest.mark.flaky # marked as flaky as the simulation might return a new readback value within the tolerance @pytest.mark.flaky # marked as flaky as the simulation might return a new readback value within the tolerance
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_mv_scan(capsys, client): def test_mv_scan(capsys, bec_client_fixture):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_mv_scan"}) bec.metadata.update({"unit_test": "test_mv_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans.mv(dev.samx, 10, dev.samy, 20, relative=False).wait() scans.mv(dev.samx, 10, dev.samy, 20, relative=False).wait()
@ -119,10 +93,9 @@ def test_mv_scan(capsys, client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_mv_scan_mv(client): def test_mv_scan_mv(bec_client_fixture):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_mv_scan_mv"}) bec.metadata.update({"unit_test": "test_mv_scan_mv"})
scan_number_start = bec.queue.next_scan_number scan_number_start = bec.queue.next_scan_number
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -184,7 +157,7 @@ def test_mv_scan_mv(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_abort(client): def test_scan_abort(bec_client_fixture):
def send_abort(bec): def send_abort(bec):
while True: while True:
current_scan_info = bec.queue.scan_storage.current_scan_info current_scan_info = bec.queue.scan_storage.current_scan_info
@ -205,8 +178,7 @@ def test_scan_abort(client):
time.sleep(0.5) time.sleep(0.5)
_thread.interrupt_main() _thread.interrupt_main()
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_abort"}) bec.metadata.update({"unit_test": "test_scan_abort"})
scan_number_start = bec.queue.next_scan_number scan_number_start = bec.queue.next_scan_number
scans = bec.scans scans = bec.scans
@ -237,9 +209,8 @@ def test_scan_abort(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_limit_error(client): def test_limit_error(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_limit_error"}) bec.metadata.update({"unit_test": "test_limit_error"})
scan_number_start = bec.queue.next_scan_number scan_number_start = bec.queue.next_scan_number
scans = bec.scans scans = bec.scans
@ -268,9 +239,8 @@ def test_limit_error(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_queued_scan(client): def test_queued_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_queued_scan"}) bec.metadata.update({"unit_test": "test_queued_scan"})
scan_number_start = bec.queue.next_scan_number scan_number_start = bec.queue.next_scan_number
scans = bec.scans scans = bec.scans
@ -301,9 +271,8 @@ def test_queued_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_fly_scan(client): def test_fly_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_fly_scan"}) bec.metadata.update({"unit_test": "test_fly_scan"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -313,9 +282,8 @@ def test_fly_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_restart(client): def test_scan_restart(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_restart"}) bec.metadata.update({"unit_test": "test_scan_restart"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -352,9 +320,8 @@ def test_scan_restart(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_observer_repeat_queued(client): def test_scan_observer_repeat_queued(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_observer_repeat_queued"}) bec.metadata.update({"unit_test": "test_scan_observer_repeat_queued"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -393,9 +360,8 @@ def test_scan_observer_repeat_queued(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_observer_repeat(client): def test_scan_observer_repeat(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_observer_repeat"}) bec.metadata.update({"unit_test": "test_scan_observer_repeat"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -432,9 +398,8 @@ def test_scan_observer_repeat(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_file_writer(client): def test_file_writer(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_file_writer"}) bec.metadata.update({"unit_test": "test_file_writer"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -479,9 +444,8 @@ def test_file_writer(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_def_callback(capsys, client): def test_scan_def_callback(capsys, bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_def_callback"}) bec.metadata.update({"unit_test": "test_scan_def_callback"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -502,9 +466,8 @@ def test_scan_def_callback(capsys, client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_scan_def(client): def test_scan_def(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_def"}) bec.metadata.update({"unit_test": "test_scan_def"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -529,9 +492,8 @@ def test_scan_def(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_group_def(client): def test_group_def(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_scan_def"}) bec.metadata.update({"unit_test": "test_scan_def"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -545,9 +507,8 @@ def test_group_def(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_list_scan(client): def test_list_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_list_scan"}) bec.metadata.update({"unit_test": "test_list_scan"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -574,9 +535,8 @@ def test_list_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_time_scan(client): def test_time_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_time_scan"}) bec.metadata.update({"unit_test": "test_time_scan"})
scans = bec.scans scans = bec.scans
status = scans.time_scan(points=5, interval=0.5, exp_time=0.1, relative=False) status = scans.time_scan(points=5, interval=0.5, exp_time=0.1, relative=False)
@ -584,9 +544,8 @@ def test_time_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_monitor_scan(client): def test_monitor_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_monitor_scan"}) bec.metadata.update({"unit_test": "test_monitor_scan"})
scans = bec.scans scans = bec.scans
dev = bec.device_manager.devices dev = bec.device_manager.devices
@ -597,9 +556,8 @@ def test_monitor_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_rpc_calls(client): def test_rpc_calls(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_rpc_calls"}) bec.metadata.update({"unit_test": "test_rpc_calls"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
assert dev.samx.dummy_controller._func_with_args(2, 3) == [2, 3] assert dev.samx.dummy_controller._func_with_args(2, 3) == [2, 3]
@ -616,9 +574,8 @@ def test_rpc_calls(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_burst_scan(client): def test_burst_scan(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_burst_scan"}) bec.metadata.update({"unit_test": "test_burst_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
s = scans.line_scan(dev.samx, 0, 1, burst_at_each_point=2, steps=10, relative=False) s = scans.line_scan(dev.samx, 0, 1, burst_at_each_point=2, steps=10, relative=False)
@ -626,9 +583,8 @@ def test_burst_scan(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_callback_data_matches_scan_data(client): def test_callback_data_matches_scan_data(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_callback_data_matches_scan_data"}) bec.metadata.update({"unit_test": "test_callback_data_matches_scan_data"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
reference_container = {"data": [], "metadata": {}} reference_container = {"data": [], "metadata": {}}
@ -649,9 +605,8 @@ def test_callback_data_matches_scan_data(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_async_callback_data_matches_scan_data(client): def test_async_callback_data_matches_scan_data(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_async_callback_data_matches_scan_data"}) bec.metadata.update({"unit_test": "test_async_callback_data_matches_scan_data"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
reference_container = {"data": [], "metadata": {}} reference_container = {"data": [], "metadata": {}}
@ -674,9 +629,8 @@ def test_async_callback_data_matches_scan_data(client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_disabled_device_raises_scan_request_error(client): def test_disabled_device_raises_scan_request_error(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_disabled_device_raises_scan_rejection"}) bec.metadata.update({"unit_test": "test_disabled_device_raises_scan_rejection"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
dev.samx.enabled = False dev.samx.enabled = False
@ -689,10 +643,9 @@ def test_disabled_device_raises_scan_request_error(client):
# @pytest.fixture(scope="function") # @pytest.fixture(scope="function")
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
@pytest.mark.parametrize("abort_on_ctrl_c", [True, False]) @pytest.mark.parametrize("abort_on_ctrl_c", [True, False])
def test_context_manager_export(tmp_path, client, abort_on_ctrl_c): def test_context_manager_export(tmp_path, bec_client_fixture, abort_on_ctrl_c):
bec = client bec = bec_client_fixture
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_line_scan"}) bec.metadata.update({"unit_test": "test_line_scan"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
bec._client._service_config = PropertyMock() bec._client._service_config = PropertyMock()
@ -705,17 +658,17 @@ def test_context_manager_export(tmp_path, client, abort_on_ctrl_c):
dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True
) )
else: else:
with scans.scan_export(os.path.join(tmp_path, "test.csv")): scan_file = os.path.join(tmp_path, "test.csv")
with scans.scan_export(scan_file):
scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.01, relative=True) scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.01, relative=True)
scans.grid_scan(dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True) scans.grid_scan(dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True)
assert len(list(tmp_path.iterdir())) == 1 assert os.path.exists(scan_file)
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_update_config(client): def test_update_config(bec_client_fixture):
bec = client bec = bec_client_fixture
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_update_config"}) bec.metadata.update({"unit_test": "test_update_config"})
demo_config_path = os.path.join(os.path.dirname(configs.__file__), "demo_config.yaml") demo_config_path = os.path.join(os.path.dirname(configs.__file__), "demo_config.yaml")
config = bec.config._load_config_from_file(demo_config_path) config = bec.config._load_config_from_file(demo_config_path)

View File

@ -1,56 +1,22 @@
import os import os
import threading
import time import time
import bec_lib
import numpy as np import numpy as np
import pytest import pytest
import yaml import yaml
from bec_lib import BECClient, DeviceConfigError, RedisConnector, ServiceConfig, bec_logger
import bec_lib
from bec_lib import DeviceConfigError, bec_logger
from bec_lib.alarm_handler import AlarmBase from bec_lib.alarm_handler import AlarmBase
from bec_lib.tests.utils import wait_for_empty_queue
logger = bec_logger.logger logger = bec_logger.logger
CONFIG_PATH = "../ci/test_config.yaml"
# CONFIG_PATH = "../bec_config_dev.yaml"
# pylint: disable=no-member
# pylint: disable=missing-function-docstring
# pylint: disable=redefined-outer-name
# pylint: disable=protected-access
# pylint: disable=undefined-variable
@pytest.fixture()
def threads_check():
current_threads = set(th for th in threading.enumerate() if th is not threading.main_thread())
yield
threads_after = set(th for th in threading.enumerate() if th is not threading.main_thread())
additional_threads = threads_after - current_threads
assert (
len(additional_threads) == 0
), f"Test creates {len(additional_threads)} threads that are not cleaned: {additional_threads}"
@pytest.fixture(scope="function")
def lib_client(threads_check):
config = ServiceConfig(CONFIG_PATH)
bec = BECClient(config, RedisConnector, forced=True)
bec.start()
bec.queue.request_queue_reset()
bec.queue.request_scan_continuation()
time.sleep(5)
yield bec
bec.shutdown()
bec._client._reset_singleton()
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_grid_scan_lib_client(lib_client): def test_grid_scan_lib(bec_client_lib):
bec = lib_client bec = bec_client_lib
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec) bec.metadata.update({"unit_test": "test_grid_scan_bec_client_lib"})
bec.metadata.update({"unit_test": "test_grid_scan_lib_client"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans.umv(dev.samx, 0, dev.samy, 0, relative=False) scans.umv(dev.samx, 0, dev.samy, 0, relative=False)
status = scans.grid_scan(dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True) status = scans.grid_scan(dev.samx, -5, 5, 10, dev.samy, -5, 5, 10, exp_time=0.01, relative=True)
@ -60,11 +26,10 @@ def test_grid_scan_lib_client(lib_client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_mv_scan_lib_client(lib_client): def test_mv_scan_lib(bec_client_lib):
bec = lib_client bec = bec_client_lib
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec) bec.metadata.update({"unit_test": "test_mv_scan_bec_client_lib"})
bec.metadata.update({"unit_test": "test_mv_scan_lib_client"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans.mv(dev.samx, 10, dev.samy, 20, relative=False).wait() scans.mv(dev.samx, 10, dev.samy, 20, relative=False).wait()
current_pos_samx = dev.samx.read()["samx"]["value"] current_pos_samx = dev.samx.read()["samx"]["value"]
@ -78,10 +43,9 @@ def test_mv_scan_lib_client(lib_client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_mv_raises_limit_error(lib_client): def test_mv_raises_limit_error(bec_client_lib):
bec = lib_client bec = bec_client_lib
scans = bec.scans scans = bec.scans
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_mv_raises_limit_error"}) bec.metadata.update({"unit_test": "test_mv_raises_limit_error"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
dev.samx.limits = [-50, 50] dev.samx.limits = [-50, 50]
@ -90,9 +54,8 @@ def test_mv_raises_limit_error(lib_client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_async_callback_data_matches_scan_data_lib_client(lib_client): def test_async_callback_data_matches_scan_data_lib(bec_client_lib):
bec = lib_client bec = bec_client_lib
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_async_callback_data_matches_scan_data"}) bec.metadata.update({"unit_test": "test_async_callback_data_matches_scan_data"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
reference_container = {"data": [], "metadata": {}} reference_container = {"data": [], "metadata": {}}
@ -114,9 +77,8 @@ def test_async_callback_data_matches_scan_data_lib_client(lib_client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_config_updates(lib_client): def test_config_updates(bec_client_lib):
bec = lib_client bec = bec_client_lib
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_config_updates"}) bec.metadata.update({"unit_test": "test_config_updates"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
dev.samx.limits = [-80, 80] dev.samx.limits = [-80, 80]
@ -150,9 +112,8 @@ def test_config_updates(lib_client):
@pytest.mark.timeout(100) @pytest.mark.timeout(100)
def test_dap_fit(lib_client): def test_dap_fit(bec_client_lib):
bec = lib_client bec = bec_client_lib
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_dap_fit"}) bec.metadata.update({"unit_test": "test_dap_fit"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans = bec.scans scans = bec.scans
@ -312,9 +273,8 @@ def test_dap_fit(lib_client):
"invalid_device_class", "invalid_device_class",
], ],
) )
def test_config_reload(lib_client, config, raises_error, deletes_config, disabled_device): def test_config_reload(bec_client_lib, config, raises_error, deletes_config, disabled_device):
bec = lib_client bec = bec_client_lib
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_config_reload"}) bec.metadata.update({"unit_test": "test_config_reload"})
try: try:
# write new config to disk # write new config to disk
@ -343,9 +303,8 @@ def test_config_reload(lib_client, config, raises_error, deletes_config, disable
# bec.config.load_demo_config() # bec.config.load_demo_config()
def test_computed_signal(lib_client): def test_computed_signal(bec_client_lib):
bec = lib_client bec = bec_client_lib
wait_for_empty_queue(bec)
bec.metadata.update({"unit_test": "test_computed_signal"}) bec.metadata.update({"unit_test": "test_computed_signal"})
dev = bec.device_manager.devices dev = bec.device_manager.devices
scans = bec.scans scans = bec.scans

View File

@ -0,0 +1,206 @@
import os
import pathlib
import platform
import tempfile
import warnings
import pytest
from pytest_redis import factories as pytest_redis_factories
try:
from bec_client import BECIPythonClient
except ImportError:
warnings.warn(
"No BEC IPython client installed, 'bec_client_fixture_with_demo_config' is not available"
)
from bec_lib import BECClient, ConfigHelper, RedisConnector, ServiceConfig
from bec_lib.tests.utils import wait_for_empty_queue
@pytest.hookimpl
def pytest_addoption(parser):
parser.addoption("--start-servers", action="store_true", default=False)
parser.addoption("--bec-redis-host", action="store", default="localhost")
parser.addoption("--bec-redis-cmd", action="store", default=None)
parser.addoption("--flush-redis", action="store_true", default=False)
parser.addoption("--files-path", action="store", default=None)
redis_server_fixture = None
bec_redis = None
_start_servers = False
bec_servers_scope = (
lambda fixture_name, config: config.getoption("--flush-redis") and "function" or "session"
)
config_template = f"""
redis:
host: %(redis_host)s
port: %(redis_port)s
mongodb:
host: "localhost"
port: 27017
scibec:
host: http://localhost
port: 3030
beamline: TestBeamline
service_config:
abort_on_ctrl_c: False
enforce_ACLs: False
file_writer:
plugin: default_NeXus_format
base_path: %(file_writer_base_path)s
"""
def _check_path(file_path):
if os.path.exists(file_path):
return pathlib.Path(file_path)
else:
raise RuntimeError(
f"end2end tests: --files-path directory {repr(file_path)} does not exist"
)
def _get_tmp_dir():
# on MacOS, gettempdir() returns path like /var/folders/nj/269977hs0_96bttwj2gs_jhhp48z54/T[...],
# and if building a Unix socket file (like pytest-redis does to connect to redis) it can
# exceed the 109 characters limit, so make a special case for MacOS
return pathlib.Path("/tmp" if platform.system() == "Darwin" else tempfile.gettempdir())
@pytest.hookimpl
def pytest_configure(config):
global redis_server_fixture
global bec_redis
global _start_servers
global _bec_servers_scope
if config.getoption("--start-servers"):
# configure 'datadir' == where redis Unix socket will go, and .rdb file (if any)
# try to use specified files path (hope it does not exceed 109 chars) or
# just use the normal tmp file directory except on MacOS where it must be enforced
# to /tmp
user_tmp_path = config.getoption("--files-path")
if user_tmp_path is not None:
datadir = _check_path(user_tmp_path)
else:
datadir = _get_tmp_dir()
# session-scoped fixture that starts redis using provided cmd
redis_server_fixture = pytest_redis_factories.proc.redis_proc(
executable=config.getoption("--bec-redis-cmd"), datadir=datadir
)
if config.getoption("--flush-redis"):
bec_redis = pytest_redis_factories.redisdb("redis_server_fixture")
_bec_servers_scope = "function" # have to restart servers at each test
else:
bec_redis = redis_server_fixture
else:
# do not automatically start redis - bec_redis will use existing
# process, will wait for 3 seconds max (must be running already);
# there is no point checking if we want to flush redis
# since it would remove available scans which are only populated
# when scan server starts
redis_server_fixture = pytest_redis_factories.redis_noproc(
host=config.getoption("--bec-redis-host"), startup_timeout=3
)
bec_redis = redis_server_fixture
_start_servers = config.getoption("--start-servers")
@pytest.fixture(scope=bec_servers_scope)
def bec_services_config_path(request):
user_tmp_path = request.config.getoption("--files-path")
if user_tmp_path is not None:
yield _check_path(user_tmp_path)
else:
if request.config.getoption("--flush-redis"):
request.fixturenames.append("tmp_path")
yield request.getfixturevalue("tmp_path")
else:
request.fixturenames.append("tmp_path_factory")
yield request.getfixturevalue("tmp_path_factory").mktemp("bec_files")
@pytest.fixture(scope=bec_servers_scope)
def bec_servers(bec_services_config_path, redis_server_fixture):
config_path = bec_services_config_path / "test_config.yaml"
file_writer_path = bec_services_config_path # / "writer_output"
# file_writer_path.mkdir(exist_ok=True)
config_content = config_template % {
"redis_host": redis_server_fixture.host,
"redis_port": redis_server_fixture.port,
"file_writer_base_path": file_writer_path,
}
with open(config_path, "w") as config_file:
config_file.write(config_content)
if _start_servers:
from bec_server.service_handler import ServiceHandler
# Start all BEC servers, kill them at the end
# when no_tmux=True, 'bec_path' indicate the cwd
# for the process (working directory), i.e. where log files will go
service_handler = ServiceHandler(
bec_path=bec_services_config_path,
config_path=config_path,
no_tmux=True,
)
processes = service_handler.start()
try:
yield
finally:
for process in processes:
process.terminate()
for process in processes:
os.waitpid(process.pid, 0)
else:
# Nothing to do here: servers are supposed to be started externally.
yield
@pytest.fixture
def bec_client_with_demo_config(bec_redis, bec_services_config_path, bec_servers):
config = ServiceConfig(bec_services_config_path / "test_config.yaml")
bec = BECIPythonClient(config, RedisConnector, forced=True)
bec.start()
ConfigHelper(bec._client.connector).load_demo_config()
try:
yield bec
finally:
bec.shutdown()
bec._client._reset_singleton()
@pytest.fixture
def bec_client_lib_with_demo_config(bec_redis, bec_services_config_path, bec_servers):
config = ServiceConfig(bec_services_config_path / "test_config.yaml")
bec = BECClient(config, RedisConnector, forced=True, wait_for_server=True)
bec.start()
ConfigHelper(bec._client.connector).load_demo_config()
try:
yield bec
finally:
bec.shutdown()
bec._client._reset_singleton()
@pytest.fixture
def bec_client_fixture(bec_client_with_demo_config):
bec = bec_client_with_demo_config
bec.queue.request_queue_reset()
bec.queue.request_scan_continuation()
wait_for_empty_queue(bec)
yield bec
@pytest.fixture
def bec_client_lib(bec_client_lib_with_demo_config):
bec = bec_client_lib_with_demo_config
bec.queue.request_queue_reset()
bec.queue.request_scan_continuation()
wait_for_empty_queue(bec)
yield bec

View File

@ -0,0 +1,56 @@
from __future__ import annotations
import copy
import threading
from unittest import mock
import pytest
from bec_lib.service_config import ServiceConfig
from bec_lib.tests.utils import ClientMock, ConnectorMock, DMClientMock, load_test_config
@pytest.fixture()
def threads_check():
threads_at_start = set(th for th in threading.enumerate() if th is not threading.main_thread())
yield
threads_after = set(th for th in threading.enumerate() if th is not threading.main_thread())
additional_threads = threads_after - threads_at_start
assert (
len(additional_threads) == 0
), f"Test creates {len(additional_threads)} threads that are not cleaned: {additional_threads}"
@pytest.fixture
def dm():
service_mock = mock.MagicMock()
service_mock.connector = ConnectorMock("")
dev_manager = DMClientMock(service_mock)
yield dev_manager
@pytest.fixture
def dm_with_devices(dm):
dm._session = copy.deepcopy(load_test_config())
dm._load_session()
yield dm
@pytest.fixture()
def bec_client_mock(dm_with_devices):
client = ClientMock(
ServiceConfig(redis={"host": "host", "port": 123}, scibec={"host": "host", "port": 123}),
ConnectorMock,
wait_for_server=False,
)
client.start()
device_manager = dm_with_devices
for name, dev in device_manager.devices.items():
dev._info["hints"] = {"fields": [name]}
client.device_manager = device_manager
try:
yield client
finally:
client.shutdown()
client._reset_singleton()
device_manager.devices.flush()

View File

@ -1,16 +1,12 @@
from __future__ import annotations from __future__ import annotations
import builtins import builtins
import copy
import functools import functools
import os import os
import threading
import time import time
import uuid import uuid
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest import mock
import pytest
import yaml import yaml
import bec_lib import bec_lib
@ -20,7 +16,6 @@ from bec_lib.devicemanager import DeviceManagerBase
from bec_lib.endpoints import EndpointInfo, MessageEndpoints from bec_lib.endpoints import EndpointInfo, MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_lib.scans import Scans from bec_lib.scans import Scans
from bec_lib.service_config import ServiceConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from bec_lib.alarm_handler import Alarms from bec_lib.alarm_handler import Alarms
@ -35,38 +30,6 @@ logger = bec_logger.logger
# pylint: disable=protected-access # pylint: disable=protected-access
@pytest.fixture(autouse=True)
def threads_check():
current_threads = set(th for th in threading.enumerate() if th is not threading.main_thread())
yield
threads_after = set(th for th in threading.enumerate() if th is not threading.main_thread())
additional_threads = threads_after - current_threads
assert (
len(additional_threads) == 0
), f"Test creates {len(additional_threads)} threads that are not cleaned: {additional_threads}"
@pytest.fixture
def dm():
service_mock = mock.MagicMock()
service_mock.connector = ConnectorMock("")
dev_manager = DMClientMock(service_mock)
yield dev_manager
@functools.lru_cache
def load_test_config():
with open(f"{dir_path}/tests/test_config.yaml", "r", encoding="utf-8") as f:
return create_session_from_config(yaml.safe_load(f))
@pytest.fixture
def dm_with_devices(dm):
dm._session = copy.deepcopy(load_test_config())
dm._load_session()
yield dm
def queue_is_empty(queue) -> bool: # pragma: no cover def queue_is_empty(queue) -> bool: # pragma: no cover
if not queue: if not queue:
return True return True
@ -491,24 +454,6 @@ class DMClientMock(DeviceManagerBase):
return dev return dev
@pytest.fixture()
def bec_client(dm_with_devices):
client = ClientMock(
ServiceConfig(redis={"host": "host", "port": 123}, scibec={"host": "host", "port": 123}),
ConnectorMock,
wait_for_server=False,
)
client.start()
print(id(client))
device_manager = dm_with_devices
for name, dev in device_manager.devices.items():
dev._info["hints"] = {"fields": [name]}
client.device_manager = device_manager
yield client
client._reset_singleton()
device_manager.devices.flush()
class PipelineMock: # pragma: no cover class PipelineMock: # pragma: no cover
_pipe_buffer = [] _pipe_buffer = []
_connector = None _connector = None
@ -668,3 +613,9 @@ def create_session_from_config(config: dict) -> dict:
device_configs.append(dev_conf) device_configs.append(dev_conf)
session = {"accessGroups": "customer", "devices": device_configs} session = {"accessGroups": "customer", "devices": device_configs}
return session return session
@functools.lru_cache
def load_test_config():
with open(f"{dir_path}/tests/test_config.yaml", "r", encoding="utf-8") as f:
return create_session_from_config(yaml.safe_load(f))

View File

@ -27,6 +27,7 @@ if __name__ == "__main__":
"dev": [ "dev": [
"pytest", "pytest",
"pytest-random-order", "pytest-random-order",
"pytest-redis",
"pytest-timeout", "pytest-timeout",
"coverage", "coverage",
"pandas", "pandas",
@ -35,7 +36,13 @@ if __name__ == "__main__":
"fakeredis", "fakeredis",
] ]
}, },
entry_points={"console_scripts": ["bec-channel-monitor = bec_lib:channel_monitor_launch"]}, entry_points={
"console_scripts": ["bec-channel-monitor = bec_lib:channel_monitor_launch"],
"pytest11": [
"bec_lib_end2end_fixtures = bec_lib.tests.end2end_fixtures",
"bec_lib_fixtures = bec_lib.tests.fixtures",
],
},
package_data={"bec_lib.tests": ["*.yaml"], "bec_lib.configs": ["*.yaml", "*.json"]}, package_data={"bec_lib.tests": ["*.yaml"], "bec_lib.configs": ["*.yaml", "*.json"]},
version=__version__, version=__version__,
) )