0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

refactor: repo reorganisation

This commit is contained in:
wyzula-jan
2023-12-12 17:25:18 +01:00
parent af995a74f3
commit f3a47a5b08
8 changed files with 5 additions and 5 deletions

484
tests/test_plot_app.py Normal file
View File

@ -0,0 +1,484 @@
import unittest
from unittest.mock import MagicMock, patch
import pyqtgraph as pg
import pytest
from qtpy.QtWidgets import QMessageBox
from bec_widgets.examples.plot_app.plot_app import PlotApp, ErrorHandler
def setup_plot_app(qtbot, config):
"""Helper function to set up the PlotApp widget."""
client = MagicMock()
widget = PlotApp(config=config, client=client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
@pytest.fixture
def error_handler():
# TODO so far tested separately, but the error message scenarios can be tested directly in the plot app
return ErrorHandler()
config_device_mode_all_filled = {
"plot_settings": {
"background_color": "black",
"num_columns": 2,
"colormap": "plasma",
"scan_types": False,
},
"plot_data": [
{
"plot_name": "BPM4i plots vs samx",
"x": {
"label": "Motor Y",
"signals": [{"name": "samx", "entry": "samx"}],
},
"y": {
"label": "bpm4i",
"signals": [{"name": "bpm4i", "entry": "bpm4i"}],
},
},
{
"plot_name": "Gauss plots vs samx",
"x": {
"label": "Motor X",
"signals": [{"name": "samx", "entry": "samx"}],
},
"y": {
"label": "Gauss",
"signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
},
},
],
}
config_device_mode_no_entry = {
"plot_settings": {
"background_color": "white",
"num_columns": 1,
"colormap": "plasma",
"scan_types": False,
},
"plot_data": [
{
"plot_name": "BPM4i plots vs samx",
"x": {
"label": "Motor Y",
"signals": [{"name": "samx"}], # Entry is missing
},
"y": {
"label": "bpm4i",
"signals": [{"name": "bpm4i"}], # Entry is missing
},
},
{
"plot_name": "Gauss plots vs samx",
"x": {
"label": "Motor X",
"signals": [{"name": "samx"}], # Entry is missing
},
"y": {
"label": "Gauss",
"signals": [{"name": "gauss_bpm"}], # Entry is missing
},
},
],
}
config_scan_mode = config = {
"plot_settings": {
"background_color": "white",
"num_columns": 3,
"colormap": "plasma",
"scan_types": True,
},
"plot_data": {
"grid_scan": [
{
"plot_name": "Grid plot 1",
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "BPM",
"signals": [
{"name": "gauss_bpm", "entry": "gauss_bpm"},
{"name": "gauss_adc1", "entry": "gauss_adc1"},
],
},
},
{
"plot_name": "Grid plot 2",
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "BPM",
"signals": [
{"name": "gauss_bpm", "entry": "gauss_bpm"},
{"name": "gauss_adc1", "entry": "gauss_adc1"},
],
},
},
{
"plot_name": "Grid plot 3",
"x": {"label": "Motor Y", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "BPM",
"signals": [{"name": "gauss_bpm", "entry": "gauss_bpm"}],
},
},
{
"plot_name": "Grid plot 4",
"x": {"label": "Motor Y", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "BPM",
"signals": [{"name": "gauss_adc3", "entry": "gauss_adc3"}],
},
},
],
"line_scan": [
{
"plot_name": "BPM plot",
"x": {"label": "Motor X", "signals": [{"name": "samx"}]},
"y": {
"label": "BPM",
"signals": [
{"name": "gauss_bpm", "entry": "gauss_bpm"},
{"name": "gauss_adc1", "entry": "gauss_adc1"},
{"name": "gauss_adc2", "entry": "gauss_adc2"},
],
},
},
{
"plot_name": "Multi",
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "Multi",
"signals": [
{"name": "gauss_bpm", "entry": "gauss_bpm"},
{"name": "samx", "entry": ["samx", "samx_setpoint"]},
],
},
},
{
"plot_name": "Multi",
"x": {"label": "Motor X", "signals": [{"name": "samx", "entry": "samx"}]},
"y": {
"label": "Multi",
"signals": [
{"name": "gauss_bpm", "entry": "gauss_bpm"},
{"name": "samx", "entry": ["samx", "samx_setpoint"]},
],
},
},
],
},
}
config_all_wrong = {
"plot_settings": {
"background_color": "white",
"num_columns": 1,
"colormap": "plasma",
"scan_types": False,
},
"plot_data": [
{
"plot_name": "BPM4i plots vs samx",
"x": {
"label": "Motor Y",
# signals are missing
},
"y": {
"label": "bpm4i",
"signals": [{"name": "bpm4i", "entry": "gauss_bpm"}], # wrong entry
},
},
],
}
@pytest.mark.parametrize(
"config, plot_setting_bg, num_plot ,pg_background",
[
(config_device_mode_all_filled, "black", 2, "k"),
(config_device_mode_no_entry, "white", 2, "w"),
(config_scan_mode, "white", 4, "w"),
],
)
def test_init_config(qtbot, config, plot_setting_bg, num_plot, pg_background):
plot_app = setup_plot_app(qtbot, config)
assert plot_app.plot_settings["background_color"] == plot_setting_bg
assert len(plot_app.plot_data) == num_plot
assert pg.getConfigOption("background") == pg_background
@pytest.mark.parametrize(
"config, num_columns_input, expected_num_columns, expected_plot_names, expected_coordinates",
[
(
config_device_mode_all_filled,
2,
2,
["BPM4i plots vs samx", "Gauss plots vs samx"],
[(0, 0), (0, 1)],
),
(
config_device_mode_all_filled,
5,
2,
["BPM4i plots vs samx", "Gauss plots vs samx"],
[(0, 0), (0, 1)],
), # num_columns greater than number of plots
(
config_device_mode_no_entry,
1,
1,
["BPM4i plots vs samx", "Gauss plots vs samx"],
[(0, 0), (1, 0)],
),
(
config_device_mode_no_entry,
2,
2,
["BPM4i plots vs samx", "Gauss plots vs samx"],
[(0, 0), (0, 1)],
),
(
config_device_mode_no_entry,
5,
2,
["BPM4i plots vs samx", "Gauss plots vs samx"],
[(0, 0), (0, 1)],
), # num_columns greater than number of plots,
(
config_scan_mode,
2,
2,
[
"Grid plot 1",
"Grid plot 2",
"Grid plot 3",
"Grid plot 4",
],
[(0, 0), (0, 1), (1, 0), (1, 1)],
),
(
config_scan_mode,
3,
3,
[
"Grid plot 1",
"Grid plot 2",
"Grid plot 3",
"Grid plot 4",
],
[(0, 0), (0, 1), (0, 2), (1, 0)],
),
(
config_scan_mode,
5,
4,
[
"Grid plot 1",
"Grid plot 2",
"Grid plot 3",
"Grid plot 4",
],
[(0, 0), (0, 1), (0, 2), (0, 3)],
), # num_columns greater than number of plots
],
)
def test_init_ui(
qtbot,
config,
num_columns_input,
expected_num_columns,
expected_plot_names,
expected_coordinates,
):
plot_app = setup_plot_app(qtbot, config)
plot_app.init_ui(num_columns_input)
# Validate number of columns
assert plot_app.plot_settings["num_columns"] == expected_num_columns
# Validate the plots are created correctly
for expected_name in expected_plot_names:
assert expected_name in plot_app.plots.keys()
# Validate the grid_coordinates
assert plot_app.grid_coordinates == expected_coordinates
def mock_getitem(dev_name):
"""Helper function to mock the __getitem__ method of the 'dev' object.""" ""
mock_instance = MagicMock()
if dev_name == "samx":
mock_instance._hints = "samx"
elif dev_name == "bpm4i":
mock_instance._hints = "bpm4i"
elif dev_name == "gauss_bpm":
mock_instance._hints = "gauss_bpm"
return mock_instance
@pytest.mark.parametrize(
"config, msg, metadata, expected_data",
[
# Case: msg does not have 'scanID'
(config_device_mode_all_filled, {"data": {}}, {}, {}),
# Case: scan_types is False, msg contains all valid fields, and entry is present in config
(
config_device_mode_all_filled,
{
"data": {
"samx": {"samx": {"value": 10}},
"bpm4i": {"bpm4i": {"value": 5}},
"gauss_bpm": {"gauss_bpm": {"value": 7}},
},
"scanID": 1,
},
{},
{
("samx", "samx", "bpm4i", "bpm4i"): {"x": [10], "y": [5]},
("samx", "samx", "gauss_bpm", "gauss_bpm"): {"x": [10], "y": [7]},
},
),
# Case: scan_types is False, msg contains all valid fields and entry is missing in config, should use hints
(
config_device_mode_no_entry,
{
"data": {
"samx": {"samx": {"value": 10}},
"bpm4i": {"bpm4i": {"value": 5}},
"gauss_bpm": {"gauss_bpm": {"value": 7}},
},
"scanID": 1,
},
{},
{
("samx", "samx", "bpm4i", "bpm4i"): {"x": [10], "y": [5]},
("samx", "samx", "gauss_bpm", "gauss_bpm"): {"x": [10], "y": [7]},
},
),
],
)
def test_on_scan_segment(qtbot, config, msg, metadata, expected_data):
plot_app = setup_plot_app(qtbot, config)
# Initialize and run test
plot_app.data = {}
plot_app.scanID = 0
# Get hints
plot_app.dev.__getitem__.side_effect = mock_getitem
plot_app.on_scan_segment(msg, metadata)
assert plot_app.data == expected_data
@pytest.mark.parametrize(
"config, msg, metadata, expected_exception_message",
[
# Case: scan_types is True, but metadata does not contain 'scan_name'
(
config_scan_mode,
{"data": {}, "scanID": 1},
{}, # No 'scan_name' in metadata
"Scan name not found in metadata. Please check the scan_name in the YAML config or in bec configuration.",
),
# Case: scan_types is True, metadata contains non-existing 'scan_name'
(
config_scan_mode,
{"data": {}, "scanID": 1},
{"scan_name": "non_existing_scan"},
"Scan name non_existing_scan not found in the YAML config. Please check the scan_name in the YAML config "
"or in bec configuration.",
),
],
)
def test_on_scan_message_error_handling(qtbot, config, msg, metadata, expected_exception_message):
plot_app = setup_plot_app(qtbot, config)
# Initialize
plot_app.init_curves = MagicMock()
plot_app.data = {}
plot_app.scanID = 0
plot_app.dev.__getitem__.side_effect = mock_getitem
with pytest.raises(ValueError) as exc_info:
plot_app.on_scan_segment(msg, metadata)
assert str(exc_info.value) == expected_exception_message
####################
# ErrorHandler tests
####################
def test_initialization(error_handler):
assert error_handler.errors == []
assert error_handler.parent is None
assert error_handler.retry_action is None
@patch("bec_widgets.examples.plot_app.plot_app.QMessageBox.critical", return_value=QMessageBox.Retry)
def test_handle_error_retry(mocked_critical, error_handler):
retry_action = MagicMock()
error_handler.set_retry_action(retry_action)
error_handler.handle_error("error message")
retry_action.assert_called_once()
@patch("bec_widgets.examples.plot_app.plot_app.QMessageBox.critical", return_value=QMessageBox.Cancel)
def test_handle_error_cancel(mocked_critical, error_handler):
retry_action = MagicMock()
with pytest.raises(SystemExit) as excinfo:
error_handler.handle_error("error message")
assert excinfo.value.code == 1
retry_action.assert_not_called()
@pytest.mark.parametrize(
"config, expected_errors",
[
(config_device_mode_all_filled, []),
(config_device_mode_no_entry, []),
(config_scan_mode, []),
(
config_all_wrong,
["Missing 'signals' configuration for x axis in plot 0 - 'BPM4i plots vs samx'"],
),
],
)
def test_error_handler(error_handler, config, expected_errors):
# Mock QMessageBox
error_handler.handle_error = MagicMock()
# Mock logging
with unittest.mock.patch("bec_widgets.examples.plot_app.plot_app.logging") as mocked_logging:
error_handler.validate_config_file(config)
# Assert
assert error_handler.errors == expected_errors
# If there are expected errors, check if handle_error was called
if expected_errors:
error_handler.handle_error.assert_called_once()
mocked_logging.error.assert_called()
else:
mocked_logging.error.assert_not_called()
error_handler.handle_error.assert_not_called()
def test_validate_plot_config(error_handler):
plot_config = {
"x": {"label": "Motor X", "signals": []}, # empty signals list should trigger an error
"y": {"label": "Motor Y", "signals": [{"name": "samx", "entry": "samx"}]},
}
error_handler.validate_plot_config(plot_config, 0)
assert error_handler.errors == [
"'signals' configuration for x axis in plot 0 must be a non-empty list"
]