Files
bec_widgets/tests/unit_tests/test_ide_explorer.py
T

570 lines
20 KiB
Python

import os
from pathlib import Path
from unittest import mock
import pytest
from qtpy.QtWidgets import QMessageBox
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
@pytest.fixture
def ide_explorer(qtbot, tmpdir):
"""Create an IDEExplorer widget for testing"""
widget = IDEExplorer()
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_ide_explorer_initialization(ide_explorer):
"""Test the initialization of the IDEExplorer widget"""
assert ide_explorer is not None
assert "scripts" in ide_explorer.sections
assert ide_explorer.main_explorer.sections[0].title == "SCRIPTS"
def test_ide_explorer_add_local_script(ide_explorer, qtbot, tmpdir):
local_script_section = ide_explorer.main_explorer.get_section(
"SCRIPTS"
).content_widget.get_section("Local")
local_script_section.content_widget.set_directory(str(tmpdir))
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("test_file.py", True),
):
ide_explorer._add_local_script()
assert os.path.exists(os.path.join(tmpdir, "test_file.py"))
def test_shared_scripts_section_with_files(ide_explorer, tmpdir):
"""Test that shared scripts section is created when plugin directory has files"""
# Create dummy shared script files
shared_scripts_dir = tmpdir.mkdir("shared_scripts")
shared_scripts_dir.join("shared_script1.py").write("# Shared script 1")
shared_scripts_dir.join("shared_script2.py").write("# Shared script 2")
ide_explorer.clear()
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = str(shared_scripts_dir)
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should have both Local and Shared sections
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is not None
assert "read-only" in shared_section.toolTip().lower()
def test_shared_macros_section_with_files(ide_explorer, tmpdir):
"""Test that shared macros section is created when plugin directory has files"""
# Create dummy shared macro files
shared_macros_dir = tmpdir.mkdir("shared_macros")
shared_macros_dir.join("shared_macro1.py").write("""
def shared_function1():
return "shared1"
def shared_function2():
return "shared2"
""")
shared_macros_dir.join("utilities.py").write("""
def utility_function():
return "utility"
""")
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = str(shared_macros_dir)
ide_explorer.clear()
ide_explorer.sections = ["macros"]
macros_section = ide_explorer.main_explorer.get_section("MACROS")
assert macros_section is not None
# Should have both Local and Shared sections
local_section = macros_section.content_widget.get_section("Local")
shared_section = macros_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is not None
assert "read-only" in shared_section.toolTip().lower()
def test_shared_sections_not_added_when_plugin_dir_missing(ide_explorer):
"""Test that shared sections are not added when plugin directories don't exist"""
ide_explorer.clear()
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = None
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should only have Local section
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is None
def test_shared_sections_not_added_when_directory_empty(ide_explorer, tmpdir):
"""Test that shared sections are not added when plugin directory doesn't exist on disk"""
ide_explorer.clear()
# Return a path that doesn't exist
nonexistent_path = str(tmpdir.join("nonexistent"))
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = nonexistent_path
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should only have Local section since directory doesn't exist
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is None
@pytest.mark.parametrize(
"slot, signal, file_name,scope",
[
(
"_emit_file_open_scripts_local",
"file_open_requested",
"example_script.py",
"scripts/local",
),
(
"_emit_file_preview_scripts_local",
"file_preview_requested",
"example_macro.py",
"scripts/local",
),
(
"_emit_file_open_scripts_shared",
"file_open_requested",
"example_script.py",
"scripts/shared",
),
(
"_emit_file_preview_scripts_shared",
"file_preview_requested",
"example_macro.py",
"scripts/shared",
),
],
)
def test_ide_explorer_file_signals(ide_explorer, qtbot, slot, signal, file_name, scope):
"""Test that the correct signals are emitted when files are opened or previewed"""
recv = []
def recv_file_signal(file_name, scope):
recv.append((file_name, scope))
sig = getattr(ide_explorer, signal)
sig.connect(recv_file_signal)
# Call the appropriate slot
getattr(ide_explorer, slot)(file_name)
qtbot.wait(300)
# Verify the signal was emitted with correct arguments
assert recv == [(file_name, scope)]
@pytest.mark.parametrize(
"slot, signal, func_name, file_path,scope",
[
(
"_emit_file_open_macros_local",
"file_open_requested",
"example_macro_function",
"macros/local/example_macro.py",
"macros/local",
),
(
"_emit_file_preview_macros_local",
"file_preview_requested",
"example_macro_function",
"macros/local/example_macro.py",
"macros/local",
),
(
"_emit_file_open_macros_shared",
"file_open_requested",
"example_macro_function",
"macros/shared/example_macro.py",
"macros/shared",
),
(
"_emit_file_preview_macros_shared",
"file_preview_requested",
"example_macro_function",
"macros/shared/example_macro.py",
"macros/shared",
),
],
)
def test_ide_explorer_file_signals_macros(
ide_explorer, qtbot, slot, signal, func_name, file_path, scope
):
"""Test that the correct signals are emitted when macro files are opened or previewed"""
recv = []
def recv_file_signal(file_name, scope):
recv.append((file_name, scope))
sig = getattr(ide_explorer, signal)
sig.connect(recv_file_signal)
# Call the appropriate slot
getattr(ide_explorer, slot)(func_name, file_path)
qtbot.wait(300)
# Verify the signal was emitted with correct arguments
assert recv == [(file_path, scope)]
def test_ide_explorer_add_local_macro(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro through the UI"""
# Create macros section first
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("test_macro_function", True),
):
ide_explorer._add_local_macro()
# Check that the macro file was created
expected_file = os.path.join(tmpdir, "test_macro_function.py")
assert os.path.exists(expected_file)
# Check that the file contains the expected function
with open(expected_file, "r", encoding="utf-8") as f:
content = f.read()
assert "def test_macro_function():" in content
assert "test_macro_function macro" in content
def test_ide_explorer_delete_local_script(ide_explorer, tmpdir):
"""Test deleting a local script file."""
local_script_section = ide_explorer.main_explorer.get_section(
"SCRIPTS"
).content_widget.get_section("Local")
local_script_section.content_widget.set_directory(str(tmpdir))
file_path = os.path.join(tmpdir, "delete_me.py")
Path(file_path).write_text("print('delete me')", encoding="utf-8")
with mock.patch.object(QMessageBox, "question", return_value=QMessageBox.StandardButton.Yes):
ide_explorer._delete_local_script(file_path)
assert not os.path.exists(file_path)
def test_ide_explorer_delete_local_script_directory(ide_explorer, tmpdir):
"""Test deleting a local script directory."""
local_script_section = ide_explorer.main_explorer.get_section(
"SCRIPTS"
).content_widget.get_section("Local")
local_script_section.content_widget.set_directory(str(tmpdir))
script_dir = Path(tmpdir) / "subdir"
script_dir.mkdir()
nested_file = script_dir / "nested.py"
nested_file.write_text("print('nested')", encoding="utf-8")
with mock.patch.object(QMessageBox, "question", return_value=QMessageBox.StandardButton.Yes):
ide_explorer._delete_local_script(str(script_dir))
assert not script_dir.exists()
def test_ide_explorer_delete_local_macro_broadcasts_removals(ide_explorer, tmpdir):
"""Test deleting a local macro unloads loaded macros and removes the file."""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
file_path = os.path.join(tmpdir, "delete_macro.py")
Path(file_path).write_text("def delete_macro():\n pass\n", encoding="utf-8")
ide_explorer.client.macros = mock.MagicMock()
ide_explorer.client.macros._update_handler = mock.MagicMock()
ide_explorer.client.macros._update_handler.get_existing_macros.return_value = {
"delete_macro": {"fname": file_path}
}
with mock.patch.object(QMessageBox, "question", return_value=QMessageBox.StandardButton.Yes):
ide_explorer._delete_local_macro(file_path)
ide_explorer.client.macros._update_handler.broadcast.assert_called_once_with(
action="remove", name="delete_macro"
)
assert not os.path.exists(file_path)
def test_ide_explorer_delete_local_macro_directory_broadcasts_removals(ide_explorer, tmpdir):
"""Test deleting a local macro directory unloads macros from contained files."""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
macro_dir = Path(tmpdir) / "subdir"
macro_dir.mkdir()
file_path = macro_dir / "delete_macro.py"
file_path.write_text("def delete_macro():\n pass\n", encoding="utf-8")
ide_explorer.client.macros = mock.MagicMock()
ide_explorer.client.macros._update_handler = mock.MagicMock()
ide_explorer.client.macros._update_handler.get_existing_macros.return_value = {
"delete_macro": {"fname": str(file_path)}
}
with mock.patch.object(QMessageBox, "question", return_value=QMessageBox.StandardButton.Yes):
ide_explorer._delete_local_macro(str(macro_dir))
ide_explorer.client.macros._update_handler.broadcast.assert_called_once_with(
action="remove", name="delete_macro"
)
assert not macro_dir.exists()
def test_ide_explorer_rename_local_macro_path_reloads_macros(ide_explorer, tmpdir):
"""Test local macro renames refresh open-editor and macro state."""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
old_path = os.path.join(tmpdir, "old_name.py")
new_path = os.path.join(tmpdir, "new_name.py")
ide_explorer.client.macros = mock.MagicMock()
ide_explorer.client.macros._update_handler = mock.MagicMock()
with (
mock.patch.object(ide_explorer, "_rename_open_editor_path") as mock_rename_open_editor,
mock.patch.object(ide_explorer, "_broadcast_removed_macros") as mock_broadcast_removed,
):
ide_explorer._rename_local_macro_path(old_path, new_path)
mock_broadcast_removed.assert_called_once_with(old_path)
mock_rename_open_editor.assert_called_once_with(old_path, new_path)
ide_explorer.client.macros.load_all_user_macros.assert_called_once()
def test_ide_explorer_add_local_macro_invalid_name(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro with invalid function name"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Test with invalid function name (starts with number)
with (
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("123invalid", True),
),
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.warning"
) as mock_warning,
):
ide_explorer._add_local_macro()
# Should show warning message
mock_warning.assert_called_once()
# Should not create any file
assert len(os.listdir(tmpdir)) == 0
def test_ide_explorer_add_local_macro_file_exists(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro when file already exists"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Create an existing file
existing_file = Path(tmpdir) / "existing_macro.py"
existing_file.write_text("# Existing macro")
with (
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("existing_macro", True),
),
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.question",
return_value=QMessageBox.StandardButton.Yes,
) as mock_question,
):
ide_explorer._add_local_macro()
# Should ask for overwrite confirmation
mock_question.assert_called_once()
# File should be overwritten with new content
with open(existing_file, "r") as f:
content = f.read()
assert "def existing_macro():" in content
def test_ide_explorer_add_local_macro_cancelled(ide_explorer, qtbot, tmpdir):
"""Test cancelling the add local macro dialog"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# User cancels the dialog
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("", False), # User cancelled
):
ide_explorer._add_local_macro()
# Should not create any file
assert len(os.listdir(tmpdir)) == 0
def test_ide_explorer_reload_macros_success(ide_explorer, qtbot):
"""Test successful macro reloading"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Mock the client and macros
mock_client = mock.MagicMock()
mock_macros = mock.MagicMock()
mock_client.macros = mock_macros
ide_explorer.client = mock_client
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.information"
) as mock_info:
ide_explorer._reload_macros()
# Should call load_all_user_macros
mock_macros.load_all_user_macros.assert_called_once()
# Should show success message
mock_info.assert_called_once()
assert "successfully" in mock_info.call_args[0][2]
def test_ide_explorer_reload_macros_error(ide_explorer, qtbot):
"""Test macro reloading when an error occurs"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Mock client with macros that raises an exception
mock_client = mock.MagicMock()
mock_macros = mock.MagicMock()
mock_macros.load_all_user_macros.side_effect = Exception("Test error")
mock_client.macros = mock_macros
ide_explorer.client = mock_client
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.critical"
) as mock_critical:
ide_explorer._reload_macros()
# Should show error message
mock_critical.assert_called_once()
assert "Failed to reload macros" in mock_critical.call_args[0][2]
def test_ide_explorer_refresh_macro_file_local(ide_explorer, qtbot, tmpdir):
"""Test refreshing a local macro file"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Create a test macro file
macro_file = Path(tmpdir) / "test_macro.py"
macro_file.write_text("def test_function(): pass")
# Mock the refresh method
with mock.patch.object(local_macros_section.content_widget, "refresh") as mock_refresh:
ide_explorer.refresh_macro_file(str(macro_file))
# Should refresh the file browser
mock_refresh.assert_called_once_with()
def test_ide_explorer_refresh_macro_file_no_match(ide_explorer, qtbot, tmpdir):
"""Test refreshing a macro file that doesn't match any directory"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Try to refresh a file that's not in any macro directory
unrelated_file = "/some/other/path/unrelated.py"
# Mock the refresh method
with mock.patch.object(local_macros_section.content_widget, "refresh") as mock_refresh:
ide_explorer.refresh_macro_file(unrelated_file)
# Should not refresh for unrelated files
mock_refresh.assert_not_called()
def test_ide_explorer_refresh_macro_file_no_sections(ide_explorer, qtbot):
"""Test refreshing a macro file when no macro sections exist"""
ide_explorer.clear()
# Don't add macros section
# Should handle gracefully without error
ide_explorer.refresh_macro_file("/some/path/test.py")
# Test passes if no exception is raised