diff --git a/mal_gui/app.py b/mal_gui/app.py index 358dd3b..f3ecd77 100644 --- a/mal_gui/app.py +++ b/mal_gui/app.py @@ -53,7 +53,7 @@ def __init__(self, parent=None): os.makedirs(os.path.dirname(self.config_file_path), exist_ok=True) self.config = configparser.ConfigParser() - self.config.read(self.config_file_path) + self.config.read(self.config_file_path, encoding="utf-8") self.selected_lang_file = self.config.get( "Settings", "langFilePath", fallback=None ) diff --git a/mal_gui/connection_dialog.py b/mal_gui/connection_dialog.py index 1efe2e4..1225f24 100644 --- a/mal_gui/connection_dialog.py +++ b/mal_gui/connection_dialog.py @@ -65,9 +65,8 @@ def __init__( for fieldname, association in possible_assocs.items(): field = association.get_field(fieldname) - # If assoc ends with end_assets type, give that assoc - # as option in list widget - if field.asset == self.end_asset.lg_asset: + # Association endpoints also accept subassets of their declared type. + if self.end_asset.lg_asset.is_subasset_of(field.asset): assoc_list_item = QListWidgetItem( self.start_asset.name + "." @@ -91,6 +90,7 @@ def __init__( self.ok_button = QPushButton("OK") self.ok_button.clicked.connect(self.ok_button_clicked) self.ok_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.ok_button.setEnabled(self.association_list_widget.count() > 0) button_layout.addWidget(self.ok_button) self.cancel_button = QPushButton("Cancel") @@ -112,15 +112,16 @@ def filter_items(self, text): def ok_button_clicked(self): selected_item = self.association_list_widget.currentItem() + if selected_item is None: + return - if selected_item: - data = selected_item.data(Qt.UserRole) + data = selected_item.data(Qt.UserRole) - from_asset: ModelAsset = data.get("from") - to_asset: ModelAsset = data.get("to") - self.field_name: str = data.get("fieldname") + from_asset: ModelAsset = data.get("from") + to_asset: ModelAsset = data.get("to") + self.field_name: str = data.get("fieldname") - print(f"{from_asset}.{self.field_name} = {to_asset} chosen") + print(f"{from_asset}.{self.field_name} = {to_asset} chosen") self.accept() diff --git a/tests/test_connection_dialog.py b/tests/test_connection_dialog.py new file mode 100644 index 0000000..b51a1e5 --- /dev/null +++ b/tests/test_connection_dialog.py @@ -0,0 +1,86 @@ +from types import SimpleNamespace + +import pytest +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDialog + +from maltoolbox.language import LanguageGraphAsset, LanguageGraphAssociation +from maltoolbox.language.language_graph_assoc import ( + LanguageGraphAssociationField, + link_association_to_assets, +) +from maltoolbox.model import ModelAsset + +from mal_gui.connection_dialog import AssociationConnectionDialog + + +@pytest.fixture +def machine_execution_assets(app): + machine = LanguageGraphAsset("Machine") + infotainment = LanguageGraphAsset("InfotainmentSystem", own_super_asset=machine) + application = LanguageGraphAsset("VehicularApplication") + operating_system = LanguageGraphAsset( + "OperatingSystem", own_super_asset=application + ) + + association = LanguageGraphAssociation( + name="machineExecution", + left_field=LanguageGraphAssociationField(machine, "hostMachine", 0, 1), + right_field=LanguageGraphAssociationField( + application, "machineExecutedApps", 0, 0 + ), + ) + link_association_to_assets(association, machine, application) + + return { + asset.name: ModelAsset(asset.name, asset_id, asset) + for asset_id, asset in enumerate( + (machine, infotainment, application, operating_system) + ) + } + + +def make_dialog(start_asset, end_asset): + start_item = SimpleNamespace(asset=start_asset, asset_type=start_asset.type) + end_item = SimpleNamespace(asset=end_asset, asset_type=end_asset.type) + return AssociationConnectionDialog(start_item, end_item, None, None) + + +@pytest.mark.parametrize( + ("start_type", "end_type", "expected_fieldname"), + [ + ("InfotainmentSystem", "OperatingSystem", "machineExecutedApps"), + ("OperatingSystem", "InfotainmentSystem", "hostMachine"), + ("Machine", "VehicularApplication", "machineExecutedApps"), + ], +) +def test_inherited_endpoint_types_are_available( + machine_execution_assets, start_type, end_type, expected_fieldname +): + start_asset = machine_execution_assets[start_type] + end_asset = machine_execution_assets[end_type] + dialog = make_dialog(start_asset, end_asset) + + assert dialog.association_list_widget.count() == 1 + assert dialog.ok_button.isEnabled() + selected_item = dialog.association_list_widget.currentItem() + assert selected_item.data(Qt.UserRole)["fieldname"] == expected_fieldname + + dialog.ok_button_clicked() + assert dialog.result() == QDialog.Accepted + assert dialog.field_name == expected_fieldname + + start_asset.add_associated_assets(expected_fieldname, {end_asset}) + assert end_asset in start_asset.associated_assets[expected_fieldname] + + +def test_empty_selection_cannot_be_accepted(machine_execution_assets): + unrelated_asset = ModelAsset("Unrelated", 4, LanguageGraphAsset("Unrelated")) + dialog = make_dialog(machine_execution_assets["Machine"], unrelated_asset) + + assert dialog.association_list_widget.count() == 0 + assert not dialog.ok_button.isEnabled() + + dialog.ok_button_clicked() + assert dialog.result() != QDialog.Accepted + assert dialog.field_name is None