class TestSpineDBEditorWithDBMapping(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Overridden method. Runs once before all tests in this class."""
        try:
            cls.app = QApplication().processEvents()
        except RuntimeError:
            pass
        logging.basicConfig(
            stream=sys.stderr,
            level=logging.DEBUG,
            format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
        )

    def setUp(self):
        """Overridden method. Runs before each test. Makes instances of SpineDBEditor classes."""
        self._temp_dir = TemporaryDirectory()
        url = "sqlite:///" + os.path.join(self._temp_dir.name, "test.sqlite")
        with mock.patch(
                "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"
        ), mock.patch(
                "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"
        ):
            mock_settings = mock.Mock()
            mock_settings.value.side_effect = lambda *args, **kwards: 0
            self.db_mngr = SpineDBManager(mock_settings, None)
            logger = mock.MagicMock()
            self.db_map = self.db_mngr.get_db_map(url,
                                                  logger,
                                                  codename="db",
                                                  create=True)
            self.spine_db_editor = SpineDBEditor(self.db_mngr, {url: "db"})
            self.spine_db_editor.pivot_table_model = mock.MagicMock()

    def tearDown(self):
        """Overridden method. Runs after each test.
        Use this to free resources after a test if needed.
        """
        with mock.patch(
                "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"
        ) as mock_save_w_s, mock.patch(
                "spinetoolbox.spine_db_manager.QMessageBox"):
            self.spine_db_editor.close()
            mock_save_w_s.assert_called_once()
        QApplication.removePostedEvents(
            None)  # Clean up unfinished fetcher signals
        self.db_mngr.close_all_sessions()
        self.db_mngr.clean_up()
        self.db_mngr = None  # Ensure the database file is closed to allow the temporary directory to be removed.
        self.spine_db_editor.deleteLater()
        self.spine_db_editor = None
        self._temp_dir.cleanup()

    def fetch_object_tree_model(self):
        for item in self.spine_db_editor.object_tree_model.visit_all():
            if item.can_fetch_more():
                item.fetch_more()

    def test_duplicate_object_in_object_tree_model(self):
        data = dict()
        data["object_classes"] = ["fish", "dog"]
        data["relationship_classes"] = [("fish__dog", ("fish", "dog"))]
        data["objects"] = [("fish", "nemo"), ("dog", "pluto")]
        data["relationships"] = [("fish__dog", ("nemo", "pluto"))]
        data["object_parameters"] = [("fish", "color")]
        data["object_parameter_values"] = [("fish", "nemo", "color", "orange")]
        with mock.patch(
                "spinetoolbox.spine_db_manager.SpineDBManager.entity_class_icon"
        ) as mock_icon:
            mock_icon.return_value = None
            loop = QEventLoop()
            self.db_mngr.data_imported.connect(loop.quit)
            self.db_mngr.import_data({self.db_map: data})
            loop.exec_()
            loop.deleteLater()
            mock_icon.assert_called()
        self.fetch_object_tree_model()
        root_item = self.spine_db_editor.object_tree_model.root_item
        fish_item = next(
            iter(item for item in root_item.children
                 if item.display_data == "fish"))
        nemo_item = fish_item.child(0)
        with mock.patch(
                "spinetoolbox.spine_db_editor.widgets.tree_view_mixin.QInputDialog"
        ) as mock_input_dialog:
            mock_input_dialog.getText.side_effect = lambda *args, **kwargs: (
                "nemo_copy", True)
            loop = QEventLoop()
            self.db_mngr.data_imported.connect(loop.quit)
            self.spine_db_editor.duplicate_object(nemo_item.index())
            loop.exec_()
            loop.deleteLater()
        nemo_dupe = fish_item.child(1)
        self.assertEqual(nemo_dupe.display_data, "nemo_copy")
Example #2
0
class TestAddItemsDialog(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Overridden method. Runs once before all tests in this class."""
        try:
            cls.app = QApplication().processEvents()
        except RuntimeError:
            pass
        logging.basicConfig(
            stream=sys.stderr,
            level=logging.DEBUG,
            format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
        )

    def setUp(self):
        """Overridden method. Runs before each test. Makes instance of SpineDBEditor class."""
        with mock.patch(
                "spinetoolbox.spine_db_manager.QMessageBox"
        ), mock.patch(
                "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"
        ):
            self.mock_db_mngr = mock.MagicMock()
            self.mock_db_mngr.undo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.mock_db_mngr.redo_action.__getitem__.side_effect = lambda key: QAction(
            )
            self.mock_db_map = mock.MagicMock()
            self.mock_db_map.codename = "mock_db"
            self.ds_view_form = SpineDBEditor(self.mock_db_mngr,
                                              self.mock_db_map)

    def tearDown(self):
        """Overridden method. Runs after each test.
        Use this to free resources after a test if needed.
        """
        with mock.patch(
                "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"
        ) as mock_save_w_s:
            self.ds_view_form.close()
            mock_save_w_s.assert_called_once()
        self.ds_view_form.deleteLater()
        self.ds_view_form = None
        try:
            os.remove('mock_db.sqlite')
        except OSError:
            pass

    def test_add_object_classes(self):
        """Test object classes are added through the manager when accepting the dialog."""
        dialog = AddObjectClassesDialog(self.ds_view_form, self.mock_db_mngr,
                                        self.mock_db_map)
        model = dialog.model
        header = model.header
        model.fetchMore()
        self.assertEqual(
            header,
            ['object_class name', 'description', 'display icon', 'databases'])
        indexes = [
            model.index(0, header.index(field))
            for field in ('object_class name', 'databases')
        ]
        values = ['fish', 'mock_db']
        model.batch_set_data(indexes, values)

        def _add_object_classes(db_map_data):
            self.assertTrue(self.mock_db_map in db_map_data)
            data = db_map_data[self.mock_db_map]
            self.assertEqual(len(data), 1)
            item = data[0]
            self.assertTrue("name" in item)
            self.assertEqual(item["name"], "fish")

        self.mock_db_mngr.add_object_classes.side_effect = _add_object_classes
        dialog.accept()
        self.mock_db_mngr.add_object_classes.assert_called_once()

    def test_do_not_add_object_classes_with_invalid_db(self):
        """Test object classes aren't added when the database is not correct."""
        dialog = AddObjectClassesDialog(self.ds_view_form, self.mock_db_mngr,
                                        self.mock_db_map)
        self.ds_view_form.msg_error = mock.NonCallableMagicMock()
        self.ds_view_form.msg_error.attach_mock(mock.MagicMock(), "emit")
        model = dialog.model
        header = model.header
        model.fetchMore()
        self.assertEqual(
            header,
            ['object_class name', 'description', 'display icon', 'databases'])
        indexes = [
            model.index(0, header.index(field))
            for field in ('object_class name', 'databases')
        ]
        values = ['fish', 'gibberish']
        model.batch_set_data(indexes, values)
        dialog.accept()
        self.mock_db_mngr.add_object_classes.assert_not_called()
        self.ds_view_form.msg_error.emit.assert_called_with(
            "Invalid database 'gibberish' at row 1")
class TestSpineDBEditor(
    TestSpineDBEditorAddMixin,
    TestSpineDBEditorUpdateMixin,
    TestSpineDBEditorRemoveMixin,
    TestSpineDBEditorFilterMixin,
    unittest.TestCase,
):
    @staticmethod
    def _object_class(*args):
        return dict(zip(["id", "name", "description", "display_order", "display_icon"], args))

    @staticmethod
    def _object(*args):
        return dict(zip(["id", "class_id", "name", "description"], args))

    @staticmethod
    def _relationship_class(*args):
        return dict(zip(["id", "name", "object_class_id_list", "object_class_name_list"], args))

    @staticmethod
    def _relationship(*args):
        return dict(
            zip(
                ["id", "class_id", "name", "class_name", "object_class_id_list", "object_id_list", "object_name_list"],
                args,
            )
        )

    @staticmethod
    def _object_parameter_definition(*args):
        d = dict(zip(["id", "object_class_id", "object_class_name", "parameter_name"], args))
        return d

    @staticmethod
    def _relationship_parameter_definition(*args):
        d = dict(
            zip(
                [
                    "id",
                    "relationship_class_id",
                    "relationship_class_name",
                    "object_class_id_list",
                    "object_class_name_list",
                    "parameter_name",
                ],
                args,
            )
        )
        return d

    @staticmethod
    def _object_parameter_value(*args):
        d = dict(
            zip(
                [
                    "id",
                    "object_class_id",
                    "object_class_name",
                    "object_id",
                    "object_name",
                    "parameter_id",
                    "parameter_name",
                    "value",
                ],
                args,
            )
        )
        d["entity_id"] = d["object_id"]
        return d

    @staticmethod
    def _relationship_parameter_value(*args):
        d = dict(
            zip(
                [
                    "id",
                    "relationship_class_id",
                    "relationship_class_name",
                    "object_class_id_list",
                    "object_class_name_list",
                    "relationship_id",
                    "object_id_list",
                    "object_name_list",
                    "parameter_id",
                    "parameter_name",
                    "value",
                ],
                args,
            )
        )
        d["entity_id"] = d["relationship_id"]
        return d

    @classmethod
    def setUpClass(cls):
        """Overridden method. Runs once before all tests in this class."""
        try:
            cls.app = QApplication().processEvents()
        except RuntimeError:
            pass
        logging.basicConfig(
            stream=sys.stderr,
            level=logging.DEBUG,
            format='%(asctime)s %(levelname)s: %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
        )
        cls.create_mock_dataset()

    @classmethod
    def create_mock_dataset(cls):
        cls.fish_class = cls._object_class(1, "fish", "A fish.", 1, None)
        cls.dog_class = cls._object_class(2, "dog", "A dog.", 3, None)
        cls.fish_dog_class = cls._relationship_class(
            3,
            "fish__dog",
            str(cls.fish_class["id"]) + "," + str(cls.dog_class["id"]),
            cls.fish_class["name"] + "," + cls.dog_class["name"],
        )
        cls.dog_fish_class = cls._relationship_class(
            4,
            "dog__fish",
            str(cls.dog_class["id"]) + "," + str(cls.fish_class["id"]),
            cls.dog_class["name"] + "," + cls.fish_class["name"],
        )
        cls.nemo_object = cls._object(1, cls.fish_class["id"], 'nemo', 'The lost one.')
        cls.pluto_object = cls._object(2, cls.dog_class["id"], 'pluto', "Mickey's.")
        cls.scooby_object = cls._object(3, cls.dog_class["id"], 'scooby', 'Scooby-Dooby-Doo.')
        cls.pluto_nemo_rel = cls._relationship(
            4,
            cls.dog_fish_class["id"],
            "dog__fish_pluto__nemo",
            cls.dog_fish_class["name"],
            str(cls.dog_class["id"]) + "," + str(cls.fish_class["id"]),
            str(cls.pluto_object["id"]) + "," + str(cls.nemo_object["id"]),
            cls.pluto_object["name"] + "," + cls.nemo_object["name"],
        )
        cls.nemo_pluto_rel = cls._relationship(
            5,
            cls.fish_dog_class["id"],
            "fish__dog_nemo__pluto",
            cls.fish_dog_class["name"],
            str(cls.fish_class["id"]) + "," + str(cls.dog_class["id"]),
            str(cls.nemo_object["id"]) + "," + str(cls.pluto_object["id"]),
            cls.nemo_object["name"] + "," + cls.pluto_object["name"],
        )
        cls.nemo_scooby_rel = cls._relationship(
            6,
            cls.fish_dog_class["id"],
            "fish__dog_nemo__scooby",
            cls.fish_dog_class["name"],
            str(cls.fish_class["id"]) + "," + str(cls.dog_class["id"]),
            str(cls.nemo_object["id"]) + "," + str(cls.scooby_object["id"]),
            cls.nemo_object["name"] + "," + cls.scooby_object["name"],
        )
        cls.water_parameter = cls._object_parameter_definition(1, cls.fish_class["id"], cls.fish_class["name"], "water")
        cls.breed_parameter = cls._object_parameter_definition(2, cls.dog_class["id"], cls.dog_class["name"], "breed")
        cls.relative_speed_parameter = cls._relationship_parameter_definition(
            3,
            cls.fish_dog_class["id"],
            cls.fish_dog_class["name"],
            cls.fish_dog_class["object_class_id_list"],
            cls.fish_dog_class["object_class_name_list"],
            "relative_speed",
        )
        cls.combined_mojo_parameter = cls._relationship_parameter_definition(
            4,
            cls.dog_fish_class["id"],
            cls.dog_fish_class["name"],
            cls.dog_fish_class["object_class_id_list"],
            cls.dog_fish_class["object_class_name_list"],
            "combined_mojo",
        )
        cls.nemo_water = cls._object_parameter_value(
            1,
            cls.water_parameter["object_class_id"],
            cls.water_parameter["object_class_name"],
            cls.nemo_object["id"],
            cls.nemo_object["name"],
            cls.water_parameter["id"],
            cls.water_parameter["parameter_name"],
            '"salt"',
        )
        cls.pluto_breed = cls._object_parameter_value(
            2,
            cls.breed_parameter["object_class_id"],
            cls.breed_parameter["object_class_name"],
            cls.pluto_object["id"],
            cls.pluto_object["name"],
            cls.breed_parameter["id"],
            cls.breed_parameter["parameter_name"],
            '"bloodhound"',
        )
        cls.scooby_breed = cls._object_parameter_value(
            3,
            cls.breed_parameter["object_class_id"],
            cls.breed_parameter["object_class_name"],
            cls.scooby_object["id"],
            cls.scooby_object["name"],
            cls.breed_parameter["id"],
            cls.breed_parameter["parameter_name"],
            '"great dane"',
        )
        cls.nemo_pluto_relative_speed = cls._relationship_parameter_value(
            4,
            cls.relative_speed_parameter["relationship_class_id"],
            cls.relative_speed_parameter["relationship_class_name"],
            cls.relative_speed_parameter["object_class_id_list"],
            cls.relative_speed_parameter["object_class_name_list"],
            cls.nemo_pluto_rel["id"],
            cls.nemo_pluto_rel["object_id_list"],
            cls.nemo_pluto_rel["object_name_list"],
            cls.relative_speed_parameter["id"],
            cls.relative_speed_parameter["parameter_name"],
            "-1",
        )
        cls.nemo_scooby_relative_speed = cls._relationship_parameter_value(
            5,
            cls.relative_speed_parameter["relationship_class_id"],
            cls.relative_speed_parameter["relationship_class_name"],
            cls.relative_speed_parameter["object_class_id_list"],
            cls.relative_speed_parameter["object_class_name_list"],
            cls.nemo_scooby_rel["id"],
            cls.nemo_scooby_rel["object_id_list"],
            cls.nemo_scooby_rel["object_name_list"],
            cls.relative_speed_parameter["id"],
            cls.relative_speed_parameter["parameter_name"],
            "5",
        )
        cls.pluto_nemo_combined_mojo = cls._relationship_parameter_value(
            6,
            cls.combined_mojo_parameter["relationship_class_id"],
            cls.combined_mojo_parameter["relationship_class_name"],
            cls.combined_mojo_parameter["object_class_id_list"],
            cls.combined_mojo_parameter["object_class_name_list"],
            cls.pluto_nemo_rel["id"],
            cls.pluto_nemo_rel["object_id_list"],
            cls.pluto_nemo_rel["object_name_list"],
            cls.combined_mojo_parameter["id"],
            cls.combined_mojo_parameter["parameter_name"],
            "100",
        )

    def setUp(self):
        """Overridden method. Runs before each test. Makes instances of SpineDBEditor classes."""
        with mock.patch("spinetoolbox.spine_db_manager.DiffDatabaseMapping") as mock_DiffDBMapping, mock.patch(
            "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.restore_ui"
        ), mock.patch("spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.show"):
            mock_settings = mock.Mock()
            mock_settings.value.side_effect = lambda *args, **kwards: 0
            self.db_mngr = SpineDBManager(mock_settings, None)
            self.db_mngr.fetch_db_maps_for_listener = lambda *args: None

            def DiffDBMapping_side_effect(url, codename=None, upgrade=False, create=False):
                mock_db_map = mock.MagicMock()
                mock_db_map.db_url = url
                mock_db_map.codename = codename
                return mock_db_map

            mock_DiffDBMapping.side_effect = DiffDBMapping_side_effect
            self.spine_db_editor = SpineDBEditor(self.db_mngr, {"mock_url": "mock_db"})
            self.mock_db_map = self.spine_db_editor.first_db_map
            self.spine_db_editor.pivot_table_model = mock.MagicMock()

    def tearDown(self):
        """Overridden method. Runs after each test.
        Use this to free resources after a test if needed.
        """
        with mock.patch(
            "spinetoolbox.spine_db_editor.widgets.spine_db_editor.SpineDBEditor.save_window_state"
        ) as mock_save_w_s, mock.patch("spinetoolbox.spine_db_manager.QMessageBox"):
            self.spine_db_editor.close()
            mock_save_w_s.assert_called_once()
        self.spine_db_editor.db_mngr.stop_fetchers()
        QApplication.removePostedEvents(None)  # Clean up unfinished fetcher signals
        self.spine_db_editor.deleteLater()
        self.spine_db_editor = None

    def put_mock_object_classes_in_db_mngr(self):
        """Put fish and dog object classes in the db mngr."""
        object_classes = [self.fish_class, self.dog_class]
        self.db_mngr.object_classes_added.emit({self.mock_db_map: object_classes})

    def put_mock_objects_in_db_mngr(self):
        """Put nemo, pluto and scooby objects in the db mngr."""
        objects = [self.nemo_object, self.pluto_object, self.scooby_object]
        self.db_mngr.objects_added.emit({self.mock_db_map: objects})

    def put_mock_relationship_classes_in_db_mngr(self):
        """Put dog__fish and fish__dog relationship classes in the db mngr."""
        relationship_classes = [self.fish_dog_class, self.dog_fish_class]
        self.db_mngr.relationship_classes_added.emit({self.mock_db_map: relationship_classes})

    def put_mock_relationships_in_db_mngr(self):
        """Put pluto_nemo, nemo_pluto and nemo_scooby relationships in the db mngr."""
        relationships = [self.pluto_nemo_rel, self.nemo_pluto_rel, self.nemo_scooby_rel]
        self.db_mngr.relationships_added.emit({self.mock_db_map: relationships})

    def put_mock_object_parameter_definitions_in_db_mngr(self):
        """Put water and breed object parameter definitions in the db mngr."""
        parameter_definitions = [self.water_parameter, self.breed_parameter]
        with mock.patch.object(CompoundParameterModel, "_modify_data_in_filter_menus"):
            self.db_mngr.parameter_definitions_added.emit({self.mock_db_map: parameter_definitions})

    def put_mock_relationship_parameter_definitions_in_db_mngr(self):
        """Put relative speed and combined mojo relationship parameter definitions in the db mngr."""
        parameter_definitions = [self.relative_speed_parameter, self.combined_mojo_parameter]
        with mock.patch.object(CompoundParameterModel, "_modify_data_in_filter_menus"):
            self.db_mngr.parameter_definitions_added.emit({self.mock_db_map: parameter_definitions})

    def put_mock_object_parameter_values_in_db_mngr(self):
        """Put some object parameter values in the db mngr."""
        parameter_values = [self.nemo_water, self.pluto_breed, self.scooby_breed]
        with mock.patch.object(CompoundParameterModel, "_modify_data_in_filter_menus"):
            self.db_mngr.parameter_values_added.emit({self.mock_db_map: parameter_values})

    def put_mock_relationship_parameter_values_in_db_mngr(self):
        """Put some relationship parameter values in the db mngr."""
        parameter_values = [
            self.nemo_pluto_relative_speed,
            self.nemo_scooby_relative_speed,
            self.pluto_nemo_combined_mojo,
        ]
        with mock.patch.object(CompoundParameterModel, "_modify_data_in_filter_menus"):
            self.db_mngr.parameter_values_added.emit({self.mock_db_map: parameter_values})

    def put_mock_dataset_in_db_mngr(self):
        """Put mock dataset in the db mngr."""
        self.put_mock_object_classes_in_db_mngr()
        self.put_mock_objects_in_db_mngr()
        self.put_mock_relationship_classes_in_db_mngr()
        self.put_mock_relationships_in_db_mngr()
        self.put_mock_object_parameter_definitions_in_db_mngr()
        self.put_mock_relationship_parameter_definitions_in_db_mngr()
        self.put_mock_object_parameter_values_in_db_mngr()
        self.put_mock_relationship_parameter_values_in_db_mngr()
        self.fetch_object_tree_model()

    def fetch_object_tree_model(self):
        for item in self.spine_db_editor.object_tree_model.visit_all():
            if item.can_fetch_more():
                item.fetch_more()

    def test_set_object_parameter_definition_defaults(self):
        """Test that defaults are set in object parameter_definition models according the object tree selection."""
        self.spine_db_editor.init_models()
        self.put_mock_object_classes_in_db_mngr()
        self.fetch_object_tree_model()
        # Select fish item in object tree
        root_item = self.spine_db_editor.object_tree_model.root_item
        fish_item = root_item.child(0)
        fish_index = self.spine_db_editor.object_tree_model.index_from_item(fish_item)
        self.spine_db_editor.ui.treeView_object.setCurrentIndex(fish_index)
        self.spine_db_editor.ui.treeView_object.selectionModel().select(fish_index, QItemSelectionModel.Select)
        # Check default in object parameter_definition
        model = self.spine_db_editor.object_parameter_definition_model
        model.empty_model.fetchMore()
        h = model.header.index
        row_data = []
        for row in range(model.rowCount()):
            row_data.append(tuple(model.index(row, h(field)).data() for field in ("object_class_name", "database")))
        self.assertTrue(("fish", "mock_db") in row_data)

    @unittest.skip("TODO")
    def test_set_object_parameter_value_defaults(self):
        """Test that defaults are set in relationship parameter_definition
        models according the object tree selection.
        """
        self.fail()

    @unittest.skip("TODO")
    def test_set_relationship_parameter_definition_defaults(self):
        """Test that defaults are set in relationship parameter_definition
        models according the object tree selection.
        """
        self.fail()

    @unittest.skip("TODO")
    def test_set_relationship_parameter_value_defaults(self):
        """Test that defaults are set in relationship parameter_definition
        models according the object tree selection.
        """
        self.fail()