From 5c52789664686eadee274168e4ee78871e7e503a Mon Sep 17 00:00:00 2001 From: David Perl Date: Thu, 9 Apr 2026 14:35:09 +0200 Subject: [PATCH] tests: update tests --- .github/actions/bw_install/action.yml | 2 +- .../editors/bec_console/bec_console.py | 6 +- .../editors/bec_console/bec_shell_plugin.py | 2 +- .../utility/bec_term/qtermwidget_wrapper.py | 2 +- pyproject.toml | 3 + tests/unit_tests/test_bec_console.py | 128 +++++ tests/unit_tests/test_web_console.py | 476 ------------------ 7 files changed, 136 insertions(+), 483 deletions(-) create mode 100644 tests/unit_tests/test_bec_console.py delete mode 100644 tests/unit_tests/test_web_console.py diff --git a/.github/actions/bw_install/action.yml b/.github/actions/bw_install/action.yml index 5b7c0801..71dffe08 100644 --- a/.github/actions/bw_install/action.yml +++ b/.github/actions/bw_install/action.yml @@ -62,4 +62,4 @@ runs: uv pip install --system -e ./ophyd_devices uv pip install --system -e ./bec/bec_lib[dev] uv pip install --system -e ./bec/bec_ipython_client - uv pip install --system -e ./bec_widgets[dev,pyside6] + uv pip install --system -e ./bec_widgets[dev,qtermwidget] diff --git a/bec_widgets/widgets/editors/bec_console/bec_console.py b/bec_widgets/widgets/editors/bec_console/bec_console.py index 5a642f48..6491d34f 100644 --- a/bec_widgets/widgets/editors/bec_console/bec_console.py +++ b/bec_widgets/widgets/editors/bec_console/bec_console.py @@ -96,7 +96,7 @@ class BecConsoleRegistry: console_id, terminal_id = console.console_id, console.terminal_id if console_id in self._consoles: del self._consoles[console_id] - if (term_info := self._terminal_registry.get(console_id)) is None: + if (term_info := self._terminal_registry.get(terminal_id)) is None: return if console_id in term_info.registered_console_ids: term_info.registered_console_ids.remove(console_id) @@ -230,13 +230,11 @@ class BecConsole(BECWidget, QWidget): client=None, gui_id=None, startup_cmd: str | None = None, - is_bec_shell: bool = False, terminal_id: str | None = None, **kwargs, ): super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs) self._mode = ConsoleMode.INACTIVE - self._is_bec_shell = is_bec_shell self._startup_cmd = startup_cmd self._is_initialized = False self.terminal_id = terminal_id or str(uuid4()) @@ -278,7 +276,7 @@ class BecConsole(BECWidget, QWidget): self.term = _bec_console_registry.try_get_term(self) if self.term: self._set_mode(ConsoleMode.ACTIVE) - elif self.isHidden: + elif self.isHidden(): self._set_mode(ConsoleMode.HIDDEN) else: self._set_mode(ConsoleMode.INACTIVE) diff --git a/bec_widgets/widgets/editors/bec_console/bec_shell_plugin.py b/bec_widgets/widgets/editors/bec_console/bec_shell_plugin.py index e8124fc7..0a305331 100644 --- a/bec_widgets/widgets/editors/bec_console/bec_shell_plugin.py +++ b/bec_widgets/widgets/editors/bec_console/bec_shell_plugin.py @@ -22,7 +22,7 @@ class BECShellPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def createWidget(self, parent): if parent is None: - return QWidget() + return QWidget() t = BECShell(parent) return t diff --git a/bec_widgets/widgets/utility/bec_term/qtermwidget_wrapper.py b/bec_widgets/widgets/utility/bec_term/qtermwidget_wrapper.py index 6f4653e8..76496caa 100644 --- a/bec_widgets/widgets/utility/bec_term/qtermwidget_wrapper.py +++ b/bec_widgets/widgets/utility/bec_term/qtermwidget_wrapper.py @@ -22,8 +22,8 @@ def _forward(func): @wraps(func) def wrapper(self, *args, **kwargs): target = getattr(self, "_main_widget") - method = getattr(target, func.__name__[1:]) if QTermWidget: + method = getattr(target, func.__name__[1:]) return method(*args, **kwargs) else: ... diff --git a/pyproject.toml b/pyproject.toml index e319e3a6..637d46d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ dev = [ "watchdog~=6.0", "pre_commit~=4.2", ] +qtermwidget = [ + "pyside6_qtermwidget", +] [build-system] requires = ["hatchling"] diff --git a/tests/unit_tests/test_bec_console.py b/tests/unit_tests/test_bec_console.py new file mode 100644 index 00000000..abe76abf --- /dev/null +++ b/tests/unit_tests/test_bec_console.py @@ -0,0 +1,128 @@ +from unittest import mock + +import pytest +from qtpy.QtCore import Qt +from qtpy.QtGui import QHideEvent, QShowEvent +from qtpy.QtTest import QTest + +from bec_widgets.widgets.editors.bec_console.bec_console import ( + BecConsole, + BECShell, + ConsoleMode, + _bec_console_registry, +) + +from .client_mocks import mocked_client + + +@pytest.fixture +def console_widget(qtbot): + """Create a BecConsole widget.""" + widget = BecConsole(client=mocked_client, gui_id="test_console", terminal_id="test_terminal") + qtbot.addWidget(widget) + return widget + + +@pytest.fixture +def two_console_widgets_same_terminal(qtbot): + widget1 = BecConsole(client=mocked_client, gui_id="console_1", terminal_id="shared_terminal") + widget2 = BecConsole(client=mocked_client, gui_id="console_2", terminal_id="shared_terminal") + qtbot.addWidget(widget1) + qtbot.addWidget(widget2) + return widget1, widget2 + + +def test_bec_console_initialization(console_widget: BecConsole): + assert console_widget.console_id == "test_console" + assert console_widget.terminal_id == "test_terminal" + assert console_widget._mode == ConsoleMode.ACTIVE + assert console_widget.term is not None + assert console_widget._overlay.isHidden() + console_widget.show() + assert console_widget.isVisible() + assert _bec_console_registry.owner_is_visible(console_widget.terminal_id) + + +def test_bec_console_yield_terminal_ownership(console_widget): + console_widget.show() + console_widget.take_terminal_ownership() + console_widget.yield_ownership() + assert console_widget.term is None + assert console_widget._mode == ConsoleMode.INACTIVE + + +def test_bec_console_hide_event_yields_ownership(console_widget): + console_widget.take_terminal_ownership() + console_widget.hideEvent(QHideEvent()) + assert console_widget.term is None + assert console_widget._mode == ConsoleMode.HIDDEN + + +def test_bec_console_show_event_takes_ownership(console_widget): + console_widget.yield_ownership() + console_widget.showEvent(QShowEvent()) + assert console_widget.term is not None + assert console_widget._mode == ConsoleMode.ACTIVE + + +def test_bec_console_overlay_click_takes_ownership(qtbot, console_widget): + console_widget.yield_ownership() + assert console_widget._mode == ConsoleMode.HIDDEN + + QTest.mouseClick(console_widget._overlay, Qt.LeftButton) + assert console_widget.term is not None + assert console_widget._mode == ConsoleMode.ACTIVE + assert not console_widget._overlay.isVisible() + + +def test_two_consoles_shared_terminal(two_console_widgets_same_terminal): + widget1, widget2 = two_console_widgets_same_terminal + + # Widget1 takes ownership + widget1.take_terminal_ownership() + assert widget1.term is not None + assert widget1._mode == ConsoleMode.ACTIVE + assert widget2.term is None + assert widget2._mode == ConsoleMode.HIDDEN + + # Widget2 takes ownership + widget2.take_terminal_ownership() + assert widget2.term is not None + assert widget2._mode == ConsoleMode.ACTIVE + assert widget1.term is None + assert widget1._mode == ConsoleMode.HIDDEN + + +def test_bec_console_registry_cleanup(console_widget: BecConsole): + console_widget.take_terminal_ownership() + terminal_id = console_widget.terminal_id + + assert terminal_id in _bec_console_registry._terminal_registry + _bec_console_registry.unregister(console_widget) + assert terminal_id not in _bec_console_registry._terminal_registry + + +def test_bec_shell_initialization(qtbot): + widget = BECShell(gui_id="bec_shell") + qtbot.addWidget(widget) + + assert widget.console_id == "bec_shell" + assert widget.terminal_id == "bec_shell" + assert widget.startup_cmd is not None + + +def test_bec_console_write(console_widget): + console_widget.take_terminal_ownership() + with mock.patch.object(console_widget.term, "write") as mock_write: + console_widget.write("test command") + mock_write.assert_called_once_with("test command", True) + + +def test_is_owner(console_widget: BecConsole): + assert _bec_console_registry.is_owner(console_widget) + mock_console = mock.MagicMock() + mock_console.console_id = "fake_console" + _bec_console_registry._consoles["fake_console"] = mock_console + assert not _bec_console_registry.is_owner(mock_console) + mock_console.terminal_id = console_widget.terminal_id + assert not _bec_console_registry.is_owner(mock_console) diff --git a/tests/unit_tests/test_web_console.py b/tests/unit_tests/test_web_console.py deleted file mode 100644 index da9676ec..00000000 --- a/tests/unit_tests/test_web_console.py +++ /dev/null @@ -1,476 +0,0 @@ -from unittest import mock - -import pytest -from qtpy.QtCore import Qt -from qtpy.QtGui import QHideEvent -from qtpy.QtNetwork import QAuthenticator - -from bec_widgets.widgets.editors.bec_console.bec_console import ( - BecConsole, - BECShell, - ConsoleMode, - _bec_console_registry, -) - -from .client_mocks import mocked_client - - -@pytest.fixture -def mocked_server_startup(): - """Mock the web console server startup process.""" - with mock.patch( - "bec_widgets.widgets.editors.bec_console.bec_console.subprocess" - ) as mock_subprocess: - with mock.patch.object(_bec_console_registry, "_wait_for_server_port"): - _bec_console_registry._server_port = 12345 - yield mock_subprocess - - -def static_console(qtbot, client, unique_id: str | None = None): - """Fixture to provide a static unique_id for BecConsole tests.""" - if unique_id is None: - widget = BecConsole(client=client) - else: - widget = BecConsole(client=client, terminal_id=unique_id) - qtbot.addWidget(widget) - qtbot.waitExposed(widget) - return widget - - -@pytest.fixture -def console_widget(qtbot, mocked_client, mocked_server_startup): - """Create a BecConsole widget with mocked server startup.""" - yield static_console(qtbot, mocked_client) - - -@pytest.fixture -def bec_shell_widget(qtbot, mocked_client, mocked_server_startup): - """Create a BECShell widget with mocked server startup.""" - widget = BECShell(client=mocked_client) - qtbot.addWidget(widget) - qtbot.waitExposed(widget) - yield widget - - -@pytest.fixture -def console_widget_with_static_id(qtbot, mocked_client, mocked_server_startup): - """Create a BecConsole widget with a static unique ID.""" - yield static_console(qtbot, mocked_client, unique_id="test_console") - - -@pytest.fixture -def two_console_widgets_same_id(qtbot, mocked_client, mocked_server_startup): - """Create two BecConsole widgets sharing the same unique ID.""" - widget1 = static_console(qtbot, mocked_client, unique_id="shared_console") - widget2 = static_console(qtbot, mocked_client, unique_id="shared_console") - yield widget1, widget2 - - -def test_bec_console_widget_initialization(console_widget): - assert ( - console_widget.page.url().toString() - == f"http://localhost:{_bec_console_registry._server_port}" - ) - - -def test_bec_console_write(console_widget): - # Test the write method - with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js: - console_widget.write("Hello, World!") - - assert mock.call('window.term.paste("Hello, World!");') in mock_run_js.mock_calls - - -def test_bec_console_write_no_return(console_widget): - # Test the write method with send_return=False - with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js: - console_widget.write("Hello, World!", send_return=False) - - assert mock.call('window.term.paste("Hello, World!");') in mock_run_js.mock_calls - assert mock_run_js.call_count == 1 - - -def test_bec_console_send_return(console_widget): - # Test the send_return method - with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js: - console_widget.send_return() - - script = mock_run_js.call_args[0][0] - assert "new KeyboardEvent('keypress', {charCode: 13})" in script - assert mock_run_js.call_count == 1 - - -def test_bec_console_send_ctrl_c(console_widget): - # Test the send_ctrl_c method - with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js: - console_widget.send_ctrl_c() - - script = mock_run_js.call_args[0][0] - assert "new KeyboardEvent('keypress', {charCode: 3})" in script - assert mock_run_js.call_count == 1 - - -def test_bec_console_authenticate(console_widget): - # Test the _authenticate method - token = _bec_console_registry._token - mock_auth = mock.MagicMock(spec=QAuthenticator) - console_widget._authenticate(None, mock_auth) - mock_auth.setUser.assert_called_once_with("user") - mock_auth.setPassword.assert_called_once_with(token) - - -def test_bec_console_registry_wait_for_server_port(): - # Test the _wait_for_server_port method - with mock.patch.object(_bec_console_registry, "_server_process") as mock_subprocess: - mock_subprocess.stderr.readline.side_effect = [b"Starting", b"Listening on port: 12345"] - _bec_console_registry._wait_for_server_port() - assert _bec_console_registry._server_port == 12345 - - -def test_bec_console_registry_wait_for_server_port_timeout(): - # Test the _wait_for_server_port method with timeout - with mock.patch.object(_bec_console_registry, "_server_process") as mock_subprocess: - with pytest.raises(TimeoutError): - _bec_console_registry._wait_for_server_port(timeout=0.1) - - -def test_bec_console_startup_command_execution(console_widget, qtbot): - """Test that the startup command is triggered after successful initialization.""" - # Set a custom startup command - console_widget.startup_cmd = "test startup command" - - assert console_widget.startup_cmd == "test startup command" - - # Generator to simulate JS initialization sequence - def js_readiness_sequence(): - yield False # First call: not ready yet - while True: - yield True # Any subsequent calls: ready - - readiness_gen = js_readiness_sequence() - - def mock_run_js(script, callback=None): - # Check if this is the initialization check call - if "window.term !== undefined" in script and callback: - ready = next(readiness_gen) - callback(ready) - else: - # For other JavaScript calls (like paste), just call the callback - if callback: - callback(True) - - with mock.patch.object( - console_widget.page, "runJavaScript", side_effect=mock_run_js - ) as mock_run_js_method: - # Reset initialization state and start the timer - console_widget._is_initialized = False - console_widget._startup_timer.start() - - # Wait for the initialization to complete - qtbot.waitUntil(lambda: console_widget._is_initialized, timeout=3000) - - # Verify that the startup command was executed - startup_calls = [ - call - for call in mock_run_js_method.call_args_list - if "test startup command" in str(call) - ] - assert len(startup_calls) > 0, "Startup command should have been executed" - - # Verify the initialized signal was emitted - assert console_widget._is_initialized is True - assert not console_widget._startup_timer.isActive() - - -def test_bec_shell_startup_contains_gui_id(bec_shell_widget): - """Test that the BEC shell startup command includes the GUI ID.""" - bec_shell = bec_shell_widget - - assert bec_shell._is_bec_shell - assert bec_shell._unique_id == "bec_shell" - - with mock.patch.object(bec_shell.bec_dispatcher, "cli_server", None): - assert bec_shell.startup_cmd == "bec --nogui" - - with mock.patch.object(bec_shell.bec_dispatcher.cli_server, "gui_id", "test_gui_id"): - assert bec_shell.startup_cmd == "bec --gui-id test_gui_id" - - -def test_bec_console_set_readonly(console_widget): - # Test the set_readonly method - console_widget.set_readonly(True) - assert not console_widget.isEnabled() - - console_widget.set_readonly(False) - assert console_widget.isEnabled() - - -def test_bec_console_with_unique_id(console_widget_with_static_id): - """Test creating a BecConsole with a unique_id.""" - widget = console_widget_with_static_id - - assert widget._unique_id == "test_console" - assert widget._unique_id in _bec_console_registry._page_registry - page_info = _bec_console_registry.get_page_info("test_console") - assert page_info is not None - assert page_info.owner_gui_id == widget.gui_id - assert widget.gui_id in page_info.widget_ids - - -def test_bec_console_page_sharing(two_console_widgets_same_id): - """Test that two widgets can share the same page using unique_id.""" - widget1, widget2 = two_console_widgets_same_id - - # Both should reference the same page in the registry - page_info = _bec_console_registry.get_page_info("shared_console") - assert page_info is not None - assert widget1.gui_id in page_info.widget_ids - assert widget2.gui_id in page_info.widget_ids - assert widget1.page == widget2.page - - -def test_bec_console_has_ownership(console_widget_with_static_id): - """Test the has_ownership method.""" - widget = console_widget_with_static_id - - # Widget should have ownership by default - assert widget.has_ownership() - - -def test_bec_console_yield_ownership(console_widget_with_static_id): - """Test yielding ownership of a page.""" - widget = console_widget_with_static_id - - assert widget.has_ownership() - - # Yield ownership - widget.yield_ownership() - - # Widget should no longer have ownership - assert not widget.has_ownership() - page_info = _bec_console_registry.get_page_info("test_console") - assert page_info.owner_gui_id is None - # Overlay should be shown - assert widget._mode == ConsoleMode.INACTIVE - - -def test_bec_console_take_page_ownership(two_console_widgets_same_id): - """Test taking ownership of a page.""" - widget1, widget2 = two_console_widgets_same_id - - # Widget1 should have ownership initially - assert widget1.has_ownership() - assert not widget2.has_ownership() - - # Widget2 takes ownership - widget2.take_page_ownership() - - # Now widget2 should have ownership - assert not widget1.has_ownership() - assert widget2.has_ownership() - - assert widget2._mode == ConsoleMode.ACTIVE - assert widget1._mode == ConsoleMode.INACTIVE - - -def test_bec_console_hide_event_yields_ownership(qtbot, console_widget_with_static_id): - """Test that hideEvent yields ownership.""" - widget = console_widget_with_static_id - - assert widget.has_ownership() - - # Hide the widget. Note that we cannot call widget.hide() directly - # because it doesn't trigger the hideEvent in tests as widgets are - # not visible in the test environment. - widget.hideEvent(QHideEvent()) - qtbot.wait(100) # Allow event processing - - # Widget should have yielded ownership - assert not widget.has_ownership() - page_info = _bec_console_registry.get_page_info("test_console") - assert page_info.owner_gui_id is None - - -def test_bec_console_show_event_takes_ownership(console_widget_with_static_id): - """Test that showEvent takes ownership when page has no owner.""" - widget = console_widget_with_static_id - - # Yield ownership - widget.yield_ownership() - assert not widget.has_ownership() - - # Show the widget again - widget.show() - - # Widget should have reclaimed ownership - assert widget.has_ownership() - assert widget.browser.isVisible() - assert not widget.overlay.isVisible() - - -def test_bec_console_mouse_press_takes_ownership(qtbot, two_console_widgets_same_id): - """Test that clicking on overlay takes ownership.""" - widget1, widget2 = two_console_widgets_same_id - widget1.show() - widget2.show() - - # Widget1 has ownership, widget2 doesn't - assert widget1.has_ownership() - assert not widget2.has_ownership() - assert widget1.isVisible() - assert widget1._mode == ConsoleMode.ACTIVE - assert widget2._mode == ConsoleMode.INACTIVE - - qtbot.mouseClick(widget2, Qt.MouseButton.LeftButton) - - # Widget2 should now have ownership - assert widget2.has_ownership() - assert not widget1.has_ownership() - - -def test_bec_console_registry_cleanup_removes_page(console_widget_with_static_id): - """Test that the registry cleans up pages when all widgets are removed.""" - widget = console_widget_with_static_id - - assert widget._unique_id in _bec_console_registry._page_registry - - # Cleanup the widget - widget.cleanup() - - # Page should be removed from registry - assert widget._unique_id not in _bec_console_registry._page_registry - - -def test_bec_console_without_unique_id_no_page_sharing(console_widget): - """Test that widgets without unique_id don't participate in page sharing.""" - widget = console_widget - - # Widget should not be in the page registry - assert widget._unique_id is None - assert not widget.has_ownership() # Should return False for non-unique widgets - - -def test_bec_console_registry_get_page_info_nonexistent(qtbot, mocked_client): - """Test getting page info for a non-existent page.""" - page_info = _bec_console_registry.get_page_info("nonexistent") - assert page_info is None - - -def test_bec_console_take_ownership_without_unique_id(console_widget): - """Test that take_page_ownership fails gracefully without unique_id.""" - widget = console_widget - # Should not crash when taking ownership without unique_id - widget.take_page_ownership() - - -def test_bec_console_yield_ownership_without_unique_id(console_widget): - """Test that yield_ownership fails gracefully without unique_id.""" - widget = console_widget - # Should not crash when yielding ownership without unique_id - widget.yield_ownership() - - -def test_registry_yield_ownership_gui_id_not_in_instances(): - """Test registry yield_ownership returns False when gui_id not in instances.""" - result = _bec_console_registry.yield_ownership("nonexistent_gui_id") - assert result is False - - -def test_registry_yield_ownership_instance_is_none(console_widget_with_static_id): - """Test registry yield_ownership returns False when instance weakref is dead.""" - widget = console_widget_with_static_id - gui_id = widget.gui_id - - # Store the gui_id and simulate the weakref being dead - _bec_console_registry._consoles[gui_id] = lambda: None - - result = _bec_console_registry.yield_ownership(gui_id) - assert result is False - - -def test_registry_yield_ownership_unique_id_none(console_widget_with_static_id): - """Test registry yield_ownership returns False when page info's unique_id is None.""" - widget = console_widget_with_static_id - gui_id = widget.gui_id - unique_id = widget._unique_id - widget._unique_id = None - - result = _bec_console_registry.yield_ownership(gui_id) - assert result is False - - widget._unique_id = unique_id # Restore for cleanup - - -def test_registry_yield_ownership_unique_id_not_in_page_registry(console_widget_with_static_id): - """Test registry yield_ownership returns False when unique_id not in page registry.""" - widget = console_widget_with_static_id - gui_id = widget.gui_id - unique_id = widget._unique_id - widget._unique_id = "nonexistent_unique_id" - - result = _bec_console_registry.yield_ownership(gui_id) - assert result is False - - widget._unique_id = unique_id # Restore for cleanup - - -def test_registry_owner_is_visible_page_info_none(): - """Test owner_is_visible returns False when page info doesn't exist.""" - result = _bec_console_registry.owner_is_visible("nonexistent_page") - assert result is False - - -def test_registry_owner_is_visible_no_owner(console_widget_with_static_id): - """Test owner_is_visible returns False when page has no owner.""" - widget = console_widget_with_static_id - - # Yield ownership so there's no owner - widget.yield_ownership() - page_info = _bec_console_registry.get_page_info(widget._unique_id) - assert page_info.owner_gui_id is None - - result = _bec_console_registry.owner_is_visible(widget._unique_id) - assert result is False - - -def test_registry_owner_is_visible_owner_ref_none(console_widget_with_static_id): - """Test owner_is_visible returns False when owner ref doesn't exist in instances.""" - widget = console_widget_with_static_id - unique_id = widget._unique_id - - # Remove owner from instances dict - del _bec_console_registry._consoles[widget.gui_id] - - result = _bec_console_registry.owner_is_visible(unique_id) - assert result is False - - -def test_registry_owner_is_visible_owner_instance_none(console_widget_with_static_id): - """Test owner_is_visible returns False when owner instance weakref is dead.""" - widget = console_widget_with_static_id - unique_id = widget._unique_id - gui_id = widget.gui_id - - # Simulate dead weakref - _bec_console_registry._consoles[gui_id] = lambda: None - - result = _bec_console_registry.owner_is_visible(unique_id) - assert result is False - - -def test_registry_owner_is_visible_owner_visible(console_widget_with_static_id): - """Test owner_is_visible returns True when owner is visible.""" - widget = console_widget_with_static_id - widget.show() - - result = _bec_console_registry.owner_is_visible(widget._unique_id) - assert result is True - - -def test_registry_owner_is_visible_owner_not_visible(console_widget_with_static_id): - """Test owner_is_visible returns False when owner is not visible.""" - widget = console_widget_with_static_id - widget.hide() - - result = _bec_console_registry.owner_is_visible(widget._unique_id) - assert result is False