Skip to content

Commit 1afd794

Browse files
committed
added tests
1 parent 7a7458e commit 1afd794

File tree

3 files changed

+237
-27
lines changed

3 files changed

+237
-27
lines changed

rascal2/widgets/project/models.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,31 @@ def set_item_delegates(self):
462462
)
463463
self.table.setItemDelegateForColumn(2, delegates.MultiSelectLayerDelegate(self.project_widget, self.table))
464464

465+
466+
class CustomFileModel(ClassListModel):
467+
"""Classlist model for custom files."""
468+
469+
def __init__(self, classlist: RATapi.ClassList, parent: QtWidgets.QWidget):
470+
super().__init__(classlist, parent)
471+
self.func_names = {}
472+
self.headers.remove("path")
473+
474+
def columnCount(self, parent=None) -> int:
475+
return super().columnCount() + 1
476+
477+
def headerData(self, section, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole):
478+
if section == self.columnCount() - 1:
479+
return None
480+
return super().headerData(section, orientation, role)
481+
482+
def flags(self, index):
483+
flags = super().flags(index)
484+
if index.column() in [0, self.columnCount() - 1]:
485+
return QtCore.Qt.ItemFlag.NoItemFlags
486+
if self.edit_mode:
487+
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
488+
return flags
489+
465490
def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
466491
data = super().data(index, role)
467492
if role == QtCore.Qt.ItemDataRole.DisplayRole and self.index_header(index) == "filename" and self.edit_mode:
@@ -499,7 +524,7 @@ def setData(self, index, value, role=QtCore.Qt.ItemDataRole.DisplayRole):
499524
language = None
500525
func_names = None
501526
self.func_names[value] = func_names
502-
if func_names is not None:
527+
if func_names:
503528
self.classlist[row].function_name = func_names[0]
504529
if language is not None:
505530
self.classlist[row].language = language
@@ -520,31 +545,6 @@ def index_header(self, index):
520545
return super().index_header(index)
521546

522547

523-
class CustomFileModel(ClassListModel):
524-
"""Classlist model for custom files."""
525-
526-
def __init__(self, classlist: RATapi.ClassList, parent: QtWidgets.QWidget):
527-
super().__init__(classlist, parent)
528-
self.func_names = {}
529-
self.headers.remove("path")
530-
531-
def columnCount(self, parent=None) -> int:
532-
return super().columnCount() + 1
533-
534-
def headerData(self, section, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole):
535-
if section == self.columnCount() - 1:
536-
return None
537-
return super().headerData(section, orientation, role)
538-
539-
def flags(self, index):
540-
flags = super().flags(index)
541-
if index.column() in [0, self.columnCount() - 1]:
542-
return QtCore.Qt.ItemFlag.NoItemFlags
543-
if self.edit_mode:
544-
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
545-
return flags
546-
547-
548548
class CustomFileWidget(ProjectFieldWidget):
549549
classlist_model = CustomFileModel
550550

@@ -598,7 +598,7 @@ def setup_button():
598598
)
599599
)
600600

601-
editable = (language != Languages.Cpp) and (
601+
editable = (language in [Languages.Matlab, Languages.Python]) and (
602602
self.model.data(self.model.index(index, self.model.headers.index("filename") + 1)) != "Browse..."
603603
)
604604
button.setEnabled(editable)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Tests for the custom file editor."""
2+
3+
import logging
4+
import tempfile
5+
from pathlib import Path
6+
from unittest.mock import MagicMock, patch
7+
8+
import pytest
9+
from PyQt6 import Qsci, QtWidgets
10+
from RATapi.utils.enums import Languages
11+
12+
from rascal2.dialogs.custom_file_editor import CustomFileEditorDialog, edit_file, edit_file_matlab
13+
14+
parent = QtWidgets.QMainWindow()
15+
16+
17+
@pytest.fixture
18+
def custom_file_dialog():
19+
"""Fixture for a custom file dialog."""
20+
21+
def _dialog(language, tmpdir):
22+
file = Path(tmpdir, "test_file")
23+
file.write_text("Test text for a test dialog!")
24+
dlg = CustomFileEditorDialog(file, language, parent)
25+
26+
return dlg
27+
28+
return _dialog
29+
30+
31+
@patch("rascal2.dialogs.custom_file_editor.CustomFileEditorDialog.exec")
32+
def test_edit_file(exec_mock):
33+
"""Test that the dialog is executed when edit_file() is called on a valid file"""
34+
35+
with tempfile.TemporaryDirectory() as tmp:
36+
file = Path(tmp, "testfile.py")
37+
file.touch()
38+
edit_file(file, Languages.Python, parent)
39+
40+
exec_mock.assert_called_once()
41+
42+
43+
@pytest.mark.parametrize("filepath", ["dir/", "not_there.m"])
44+
@patch("rascal2.dialogs.custom_file_editor.CustomFileEditorDialog")
45+
def test_edit_incorrect_file(dialog_mock, filepath, caplog):
46+
"""A logger error should be emitted if a directory or nonexistent file is given to the editor."""
47+
48+
with tempfile.TemporaryDirectory() as tmp:
49+
file = Path(tmp, filepath)
50+
edit_file(file, Languages.Python, parent)
51+
52+
errors = [record for record in caplog.get_records("call") if record.levelno == logging.ERROR]
53+
assert len(errors) == 1
54+
assert "Attempted to edit a custom file which does not exist!" in caplog.text
55+
56+
57+
def test_edit_file_matlab():
58+
"""Assert that a file is passed to the engine when the MATLAB editor is called."""
59+
mock_engine = MagicMock()
60+
mock_engine.edit = MagicMock()
61+
mock_loader = MagicMock()
62+
mock_loader.result = MagicMock(return_value=mock_engine)
63+
with patch("rascal2.dialogs.custom_file_editor.start_matlab", return_value=mock_loader) as mock_start:
64+
with tempfile.TemporaryDirectory() as tmp:
65+
file = Path(tmp, "testfile.m")
66+
file.touch()
67+
edit_file_matlab(file)
68+
69+
mock_start.assert_called_once()
70+
mock_loader.result.assert_called_once()
71+
mock_engine.edit.assert_called_once_with(str(file))
72+
73+
74+
def test_edit_no_matlab_engine(caplog):
75+
"""A logging error should be produced if a user tries to edit a file in MATLAB with no engine available."""
76+
with patch("rascal2.dialogs.custom_file_editor.start_matlab", return_value=None) as mock_loader:
77+
with tempfile.TemporaryDirectory() as tmp:
78+
file = Path(tmp, "testfile.m")
79+
file.touch()
80+
edit_file_matlab(file)
81+
mock_loader.assert_called_once()
82+
83+
errors = [record for record in caplog.get_records("call") if record.levelno == logging.ERROR]
84+
assert len(errors) == 1
85+
assert "Attempted to edit a file in MATLAB engine, but `matlabengine` is not available." in caplog.text
86+
87+
88+
@pytest.mark.parametrize(
89+
"language, expected_lexer",
90+
[(Languages.Python, Qsci.QsciLexerPython), (Languages.Matlab, Qsci.QsciLexerMatlab), (None, type(None))],
91+
)
92+
def test_dialog_init(custom_file_dialog, language, expected_lexer):
93+
"""Ensure the custom file editor is set up correctly."""
94+
95+
with tempfile.TemporaryDirectory() as tmp:
96+
dialog = custom_file_dialog(language, tmp)
97+
98+
assert isinstance(dialog.editor.lexer(), expected_lexer)
99+
assert dialog.editor.text() == "Test text for a test dialog!"
100+
101+
102+
def test_dialog_save(custom_file_dialog):
103+
"""Text changes to the editor are saved to the file when save_file is called."""
104+
with tempfile.TemporaryDirectory() as tmp:
105+
dialog = custom_file_dialog(Languages.Python, tmp)
106+
107+
dialog.editor.setText("New test text...")
108+
dialog.save_file()
109+
110+
assert Path(tmp, "test_file").read_text() == "New test text..."

tests/widgets/project/test_models.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
import tempfile
2+
from pathlib import Path
13
from unittest.mock import MagicMock
24

35
import pydantic
46
import pytest
57
import RATapi
68
from PyQt6 import QtCore, QtWidgets
9+
from RATapi.utils.enums import Languages
710

811
import rascal2.widgets.delegates as delegates
912
import rascal2.widgets.inputs as inputs
1013
from rascal2.widgets.project.models import (
1114
ClassListModel,
15+
CustomFileModel,
16+
CustomFileWidget,
1217
DomainContrastWidget,
1318
DomainsModel,
1419
LayerFieldWidget,
@@ -390,3 +395,98 @@ def test_domains_widget_item_delegates(domains_classlist):
390395
widget.update_model(domains_classlist)
391396
assert isinstance(widget.table.itemDelegateForColumn(1), delegates.ValidatedInputDelegate)
392397
assert isinstance(widget.table.itemDelegateForColumn(2), delegates.MultiSelectLayerDelegate)
398+
399+
400+
def test_file_model_filename_data():
401+
"""Tests the display data for the CustomFileModel `filename` field is as expected."""
402+
init_list = RATapi.ClassList(
403+
[
404+
RATapi.models.CustomFile(filename="myfile.m", path="/home/user/"),
405+
RATapi.models.CustomFile(filename="", path="/"),
406+
]
407+
)
408+
409+
model = CustomFileModel(init_list, parent)
410+
411+
filename_col = model.headers.index("filename") + 1
412+
413+
assert model.data(model.index(0, filename_col)) == "myfile.m"
414+
assert model.data(model.index(1, filename_col)) == ""
415+
416+
model.edit_mode = True
417+
418+
assert Path(model.data(model.index(0, filename_col))) == Path("/home/user/myfile.m")
419+
assert model.data(model.index(1, filename_col)) == "Browse..."
420+
421+
422+
@pytest.mark.parametrize(
423+
"filename, expected_lang, expected_filenames",
424+
(
425+
["myfile.m", Languages.Matlab, None],
426+
["myfile.py", Languages.Python, ["func1", "func2", "func3"]],
427+
["myfile.dll", Languages.Cpp, None],
428+
["myfile.so", Languages.Cpp, None],
429+
["myfile.dylib", Languages.Cpp, None],
430+
),
431+
)
432+
def test_file_model_set_filename(filename, expected_lang, expected_filenames):
433+
"""Test the custom file row autocompletes when a filename is set."""
434+
init_list = RATapi.ClassList([RATapi.models.CustomFile(filename="", path="/")])
435+
436+
python_file = "def func1(): pass \ndef func2(): pass \ndef func3(): pass"
437+
438+
model = CustomFileModel(init_list, parent)
439+
440+
filename_col = model.headers.index("filename") + 1
441+
442+
with tempfile.TemporaryDirectory() as tmp:
443+
Path(tmp, "myfile.py").write_text(python_file)
444+
filepath = Path(tmp, filename)
445+
model.setData(model.index(0, filename_col), filepath)
446+
447+
assert model.classlist[0].path == Path(tmp)
448+
assert model.classlist[0].filename == filename
449+
assert model.classlist[0].language == expected_lang
450+
451+
if expected_lang == Languages.Python:
452+
assert model.func_names[filepath] == expected_filenames
453+
assert model.classlist[0].function_name == "func1"
454+
455+
456+
@pytest.mark.parametrize("filename", ["file.m", "file.py", "file.dll", ""])
457+
def test_file_widget_edit(filename):
458+
"""Test that the correct index widget is created in edit mode."""
459+
460+
with tempfile.TemporaryDirectory() as tmp:
461+
Path(tmp, "file.py").touch()
462+
init_list = RATapi.ClassList([RATapi.models.CustomFile(filename="")])
463+
464+
widget = CustomFileWidget("files", parent)
465+
widget.update_model(init_list)
466+
467+
edit_col = widget.model.columnCount() - 1
468+
assert widget.table.isColumnHidden(edit_col)
469+
470+
widget.edit()
471+
472+
assert not widget.table.isColumnHidden(edit_col)
473+
474+
if filename != "":
475+
widget.model.setData(
476+
widget.model.index(0, widget.model.headers.index("filename") + 1),
477+
Path(tmp, filename),
478+
)
479+
480+
button = widget.table.indexWidget(widget.model.index(0, edit_col))
481+
assert isinstance(button, QtWidgets.QPushButton)
482+
483+
if filename in ["file.m", "file.py"]:
484+
assert button.isEnabled()
485+
else:
486+
assert not button.isEnabled()
487+
488+
if filename == "file.m":
489+
assert button.menu() is not None
490+
assert len(button.menu().actions()) == 2
491+
else:
492+
assert button.menu() is None

0 commit comments

Comments
 (0)