# Copyright 2022 (c) Anna Schumaker """Tests our database object base classes.""" import unittest import unittest.mock import emmental.db.table import emmental.store import tests.util.table from gi.repository import GObject from gi.repository import Gio from gi.repository import Gtk class TestRow(unittest.TestCase): """Tests our common database Row object.""" def setUp(self): """Set up common variables.""" self.table = Gio.ListStore() self.table.delete = unittest.mock.Mock(return_value=True) self.table.update = unittest.mock.Mock(return_value=True) self.row = tests.util.table.MockRow(table=self.table) def test_init(self): """Test that the database Row is configured correctly.""" self.assertIsInstance(self.row, emmental.db.table.Row) self.assertIsInstance(self.row, GObject.GObject) self.assertEqual(self.row.table, self.table) def test_primary_key(self): """Test the primary_key property.""" with self.assertRaises(NotImplementedError): emmental.db.table.Row(self.table).primary_key self.row.number = 2 self.assertEqual(self.row.primary_key, 2) def test_do_update(self): """Test updating a Row attribute.""" self.row.number = 1 self.table.update.assert_called_with(self.row, "number", 1) self.table.update.reset_mock() self.row.table = None self.table.update.assert_not_called() def test_delete(self): """Test deleting a Row.""" self.assertTrue(self.row.delete()) self.table.delete.assert_called_with(self.row) @unittest.mock.patch("gi.repository.Gtk.Filter.changed") class TestKeySet(unittest.TestCase): """Tests our KeySet for holding database Rows.""" def setUp(self): """Set up common variables.""" self.keyset = emmental.db.table.KeySet() self.table = Gio.ListStore() self.row1 = tests.util.table.MockRow(number=1, table=self.table) self.row2 = tests.util.table.MockRow(number=2, table=self.table) def test_init(self, mock_changed: unittest.mock.Mock): """Test that the KeySet is created correctly.""" self.assertIsInstance(self.keyset, Gtk.Filter) self.assertIsNone(self.keyset._keys, None) self.assertEqual(self.keyset.n_keys, -1) keyset2 = emmental.db.table.KeySet(keys={1, 2, 3}) self.assertSetEqual(keyset2._keys, {1, 2, 3}) self.assertEqual(keyset2.n_keys, 3) def test_subtract(self, mock_changed: unittest.mock.Mock): """Test subtracting two KeySets.""" keyset2 = emmental.db.table.KeySet(keys={2, 3}) self.assertIsNone(self.keyset - self.keyset) self.assertIsNone(self.keyset - keyset2) self.assertSetEqual(keyset2 - self.keyset, {2, 3}) self.keyset.keys = {1, 2, 3, 4, 5} self.assertSetEqual(self.keyset - keyset2, {1, 4, 5}) self.assertSetEqual(keyset2 - self.keyset, set()) def test_strictness(self, mock_changed: unittest.mock.Mock): """Test checking strictness.""" self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.ALL) self.keyset._keys = set() self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.NONE) self.keyset._keys = {1, 2, 3} self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.SOME) def test_add_row(self, mock_changed: unittest.mock.Mock): """Test adding Rows to the KeySet.""" mock_added = unittest.mock.Mock() self.keyset.connect("key-added", mock_added) self.keyset.add_row(self.row1) self.assertIsNone(self.keyset.keys) mock_added.assert_not_called() self.keyset.keys = set() self.keyset.add_row(self.row1) self.assertSetEqual(self.keyset.keys, {1}) self.assertEqual(self.keyset.n_keys, 1) mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT) mock_added.assert_called_with(self.keyset, 1) self.keyset.add_row(self.row2) self.assertSetEqual(self.keyset.keys, {1, 2}) self.assertEqual(self.keyset.n_keys, 2) mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT) mock_added.assert_called_with(self.keyset, 2) mock_changed.reset_mock() mock_added.reset_mock() self.keyset.add_row(self.row2) self.assertSetEqual(self.keyset.keys, {1, 2}) mock_changed.assert_not_called() mock_added.assert_not_called() def test_remove_row(self, mock_changed: unittest.mock.Mock): """Test removing Rows from the KeySet.""" mock_removed = unittest.mock.Mock() self.keyset.connect("key-removed", mock_removed) self.keyset.remove_row(self.row1) mock_changed.assert_not_called() mock_removed.assert_not_called() self.keyset.keys = {1, 2} self.keyset.remove_row(self.row1) self.assertSetEqual(self.keyset._keys, {2}) self.assertEqual(self.keyset.n_keys, 1) mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT) mock_removed.assert_called_with(self.keyset, 1) mock_changed.reset_mock() mock_removed.reset_mock() self.keyset.remove_row(self.row1) self.assertSetEqual(self.keyset.keys, {2}) self.assertEqual(self.keyset.n_keys, 1) mock_changed.assert_not_called() mock_removed.assert_not_called() self.keyset.remove_row(self.row2) self.assertSetEqual(self.keyset._keys, set()) self.assertEqual(self.keyset.n_keys, 0) mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT) mock_removed.assert_called_with(self.keyset, 2) def test_keys(self, mock_changed: unittest.mock.Mock): """Test getting and setting the KeySet.keys property.""" mock_keys_changed = unittest.mock.Mock() self.keyset.connect("keys-changed", mock_keys_changed) self.assertIsNone(self.keyset.keys) self.keyset.keys = None self.assertIsNone(self.keyset.keys) mock_changed.assert_not_called() mock_keys_changed.assert_not_called() self.keyset.keys = {1, 2, 3} self.assertSetEqual(self.keyset._keys, {1, 2, 3}) self.assertEqual(self.keyset.n_keys, 3) mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT) mock_keys_changed.assert_called_with(self.keyset, set(), {1, 2, 3}) mock_changed.reset_mock() self.keyset.keys = {1, 2} self.assertSetEqual(self.keyset.keys, {1, 2}) self.assertEqual(self.keyset.n_keys, 2) mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT) mock_keys_changed.assert_called_with(self.keyset, {3}, set()) mock_changed.reset_mock() mock_keys_changed.reset_mock() self.keyset.keys = {1, 2} mock_changed.assert_not_called() mock_keys_changed.assert_not_called() self.keyset.keys = {1, 2, 3} self.assertSetEqual(self.keyset.keys, {1, 2, 3}) mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT) mock_keys_changed.assert_called_with(self.keyset, set(), {3}) self.keyset.keys = {4, 5, 6} self.assertSetEqual(self.keyset._keys, {4, 5, 6}) mock_changed.assert_called_with(Gtk.FilterChange.DIFFERENT) mock_keys_changed.assert_called_with(self.keyset, {1, 2, 3}, {4, 5, 6}) self.keyset.keys = None self.assertIsNone(self.keyset._keys) self.assertEqual(self.keyset.n_keys, -1) mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT) mock_keys_changed.assert_called_with(self.keyset, {4, 5, 6}, set()) def test_match_contains(self, mock_changed: unittest.mock.Mock): """Test matching Rows and the __contains__() magic method.""" self.assertTrue(self.keyset.match(self.row1)) self.assertTrue(self.row1 in self.keyset) self.keyset.keys = {1, 2, 3} self.assertTrue(self.keyset.match(self.row1)) self.assertTrue(self.row1 in self.keyset) self.keyset.keys = {4, 5, 6} self.assertFalse(self.keyset.match(self.row1)) self.assertFalse(self.row1 in self.keyset) self.keyset.keys = set() self.assertFalse(self.keyset.match(self.row1)) self.assertFalse(self.row1 in self.keyset) class TestTable(tests.util.TestCase): """Tests the base Table object.""" def setUp(self): """Set up common variables.""" super().setUp() self.table = emmental.db.table.Table(self.sql) def test_init(self): """Test that the table is set up properly.""" self.assertIsInstance(self.table, Gtk.FilterListModel) self.assertIsInstance(self.table.queue, emmental.db.idle.Queue) self.assertIsInstance(self.table.get_filter(), emmental.db.table.KeySet) self.assertIsInstance(self.table.store, emmental.store.SortedList) self.assertIsInstance(self.table.rows, dict) self.assertEqual(self.table.sql, self.sql) self.assertEqual(self.table.get_model(), self.table.store) self.assertEqual(self.table.store.key_func, self.table.get_sort_key) self.assertDictEqual(self.table.rows, {}) self.assertFalse(self.table.get_incremental()) filter2 = emmental.db.table.KeySet() queue2 = emmental.db.idle.Queue() table2 = emmental.db.table.Table(self.sql, filter=filter2, queue=queue2) self.assertEqual(table2.get_filter(), filter2) self.assertEqual(table2.queue, queue2) def test_clear(self): """Test clearing a table.""" row = tests.util.table.MockRow(number=1, table=self.table) self.table.store.append(row) self.table.loaded = True self.table.queue.running = True self.table.clear() self.assertEqual(self.table.store.n_items, 0) self.assertDictEqual(self.table.rows, dict()) self.assertFalse(self.table.queue.running) self.assertFalse(self.table.loaded) def test_contains(self): """Test checking if a Row is already in this Table.""" row1 = tests.util.table.MockRow(number=1, table=self.table) row2 = tests.util.table.MockRow(number=2, table=self.table) self.table.insert(row1) self.assertTrue(row1 in self.table) self.assertFalse(row2 in self.table) def test_get_sort_key(self): """Test getting a sort key for a row.""" row = tests.util.table.MockRow(number=1, table=self.table) self.table.insert(row) self.assertEqual(self.table.get_sort_key(row), 1) def test_index(self): """Test finding the index of rows in the table.""" row1 = tests.util.table.MockRow(number=1, table=self.table) row2 = tests.util.table.MockRow(number=2, table=self.table) row3 = tests.util.table.MockRow(number=3, table=self.table) self.table.insert(row1) self.table.rows[row3.primary_key] = row3 self.assertEqual(self.table.index(row1), 0) self.assertIsNone(self.table.index(row2)) self.assertIsNone(self.table.index(row3)) def test_insert(self): """Test inserting rows into the table in sorted position.""" row1 = tests.util.table.MockRow(number=1, table=self.table) row2 = tests.util.table.MockRow(number=2, table=self.table) row3 = tests.util.table.MockRow(number=3, table=self.table) self.assertEqual(self.table.insert(row1), row1) self.assertEqual(self.table.store.get_item(0), row1) self.assertDictEqual(self.table.rows, {1: row1}) self.assertEqual(self.table.insert(row3), row3) self.assertEqual(self.table.store.get_item(0), row1) self.assertEqual(self.table.store.get_item(1), row3) self.assertDictEqual(self.table.rows, {1: row1, 3: row3}) self.assertEqual(self.table.insert(row2), row2) self.assertEqual(self.table.store.get_item(0), row1) self.assertEqual(self.table.store.get_item(1), row2) self.assertEqual(self.table.store.get_item(2), row3) self.assertDictEqual(self.table.rows, {1: row1, 2: row2, 3: row3}) row1_again = tests.util.table.MockRow(number=1, table=self.table) self.assertIsNone(self.table.insert(row1_again)) self.assertIsNone(self.table.insert(row1)) self.assertIsNone(self.table.insert(None)) def test_interface(self): """Test that calling interface functions raises an exception.""" with self.assertRaises(NotImplementedError): self.table.construct(rowid=1) with self.assertRaises(NotImplementedError): self.table.create(rowid=1) with self.assertRaises(NotImplementedError): self.table.do_sql_delete(None) with self.assertRaises(NotImplementedError): self.table.filter("*text*") self.table.queue.complete() with self.assertRaises(NotImplementedError): self.table.load() self.table.queue.complete() with self.assertRaises(NotImplementedError): self.table.lookup(1) with self.assertRaises(NotImplementedError): self.table.update(None, "column", 12345) class TestTableFunctions(tests.util.TestCase): """Tests Table functions with a Mock implementation.""" def setUp(self): """Set up common variables.""" super().setUp() self.table = tests.util.table.MockTable(self.sql) def test_construct(self): """Test constructing a new Row object.""" row = self.table.construct(number=1) self.assertIsInstance(row, tests.util.table.MockRow) self.assertIsInstance(row, emmental.db.table.Row) self.assertEqual(row.table, self.table) self.assertEqual(row.number, 1) def test_create(self): """Test creating new rows.""" row = self.table.create(number=1) self.assertIsInstance(row, tests.util.table.MockRow) self.assertEqual(self.table.index(row), 0) self.assertEqual(row.number, 1) self.assertDictEqual(self.table.rows, {1: row}) self.assertIsNone(self.table.create(number=1)) def test_delete(self): """Test deleting rows.""" row = self.table.create(number=1) with unittest.mock.patch.object(self.sql, "commit") as mock_commit: self.assertTrue(row.delete()) self.assertEqual(len(self.table), 0) self.assertDictEqual(self.table.rows, dict()) mock_commit.assert_called() self.assertFalse(row.delete()) def test_filter(self): """Test filtering Rows in the table.""" for n in [1, 121, 212, 333]: self.table.create(number=n) self.table.queue.push(unittest.mock.Mock()) self.table.filter("*3*") self.assertIsNone(self.table.get_filter().keys) self.assertEqual(self.table.queue[0], (self.table._filter_idle, "*3*")) self.table.filter("*2*") self.assertEqual(self.table.queue[0], (self.table._filter_idle, "*2*")) self.table.queue.complete() self.assertSetEqual(self.table.get_filter().keys, {121, 212}) self.table.filter("*1*", now=True) self.assertSetEqual(self.table.get_filter().keys, {1, 121, 212}) self.table.filter(None) self.assertIsNone(self.table.queue[0]) self.assertIsNone(self.table.get_filter().keys) def test_get_sort_key(self): """Test getting a sort key for a row.""" row = self.table.create(number=42) self.assertTupleEqual(self.table.get_sort_key(row), (42, 42)) def test_load(self): """Test loading rows from the database.""" self.assertFalse(self.table.loaded) table_loaded = unittest.mock.Mock() self.sql.connect("table-loaded", table_loaded) self.sql("INSERT INTO mock_table (number) VALUES (?)", 1) self.sql("INSERT INTO mock_table (number) VALUES (?)", 2) self.table.load() self.assertFalse(self.table.loaded) self.assertEqual(len(self.table), 0) self.assertEqual(self.table.queue[0], (self.table._load_idle,)) self.table.queue.complete() self.assertTrue(self.table.loaded) self.assertEqual(len(self.table), 2) table_loaded.assert_called_with(self.sql, self.table) row1 = self.table[0] row2 = self.table[1] for row, n in [(row1, 1), (row2, 2)]: with self.subTest(n=n): self.assertEqual(row.number, n) self.assertEqual(self.table.rows, {1: row1, 2: row2}) self.table.load(now=True) self.assertNotEqual(self.table[0], row1) self.assertNotEqual(self.table[1], row2) def test_lookup(self): """Test looking up rows in the table.""" row = self.table.create(number=1) self.assertEqual(self.table.lookup(1), row) self.assertIsNone(self.table.lookup(2)) def test_stop(self): """Test the table.stop() function.""" with unittest.mock.patch.object(self.table.queue, "cancel") as cancel: self.table.stop() cancel.assert_called() def test_update(self): """Test updating a Row.""" row = self.table.create(number=1) self.assertTrue(self.table.update(row, "number", 2)) row.number = 2 self.table.create(number=3) self.assertFalse(self.table.update(row, "number", 3)) class TestTableSubset(tests.util.TestCase): """Tests the TableSubset.""" def setUp(self): """Set up common variables.""" super().setUp() self.table = tests.util.table.MockTable(self.sql) self.subset = emmental.db.table.TableSubset(self.table) self.rows = [self.table.create(number=i) for i in range(5)] def test_init(self): """Test that the TableSubset was set up properly.""" self.assertIsInstance(self.subset, Gio.ListModel) self.assertIsInstance(self.subset, GObject.GObject) self.assertIsInstance(self.subset.keyset, emmental.db.table.KeySet) self.assertSetEqual(self.subset.keyset.keys, set()) self.assertEqual(self.subset.table, self.table) subset2 = emmental.db.table.TableSubset(self.table, keys={1, 2, 3}) self.assertSetEqual(subset2.keyset.keys, {1, 2, 3}) def test_get_item_type(self): """Test the Gio.ListModel.get_item_type() function.""" self.assertEqual(self.subset.get_item_type(), emmental.db.table.Row.__gtype__) def test_get_n_items(self): """Test the Gio.ListModel.get_n_items() function.""" self.assertEqual(self.subset.get_n_items(), 0) self.assertEqual(self.subset.n_rows, 0) self.subset.add_row(self.rows[0]) self.assertEqual(self.subset.get_n_items(), 0) self.assertEqual(self.subset.n_rows, 0) self.table.loaded = True self.assertEqual(self.subset.get_n_items(), 1) self.assertEqual(self.subset.n_rows, 1) self.table.loaded = False self.assertEqual(self.subset.get_n_items(), 0) self.assertEqual(self.subset.n_rows, 0) def test_get_item(self): """Test the Gio.ListModel.get_item() function.""" for row in self.rows: self.subset.add_row(row) self.assertListEqual(self.subset._items, []) for i, row in enumerate(self.rows): with self.subTest(i=i, row=row.number): self.assertIsNone(self.subset.get_item(i)) self.table.loaded = True self.assertEqual(self.subset.get_item(i), row) self.assertEqual(self.subset._items[i], row) self.table.loaded = False self.assertIsNone(self.subset.get_item(i)) def test_add_row(self): """Test adding a row to the TableSubset.""" expected = set() self.table.loaded = True self.assertListEqual(self.subset._items, []) changed = unittest.mock.Mock() self.subset.connect("items-changed", changed) for n, i in enumerate([2, 0, 4, 1, 3], start=1): row = self.rows[i] with self.subTest(i=i, row=row.number): expected.add(i) self.subset.add_row(row) self.assertSetEqual(self.subset.keyset.keys, expected) self.assertEqual(self.subset.n_rows, n) changed.assert_called_with(self.subset, sorted(expected).index(i), 0, 1) self.assertListEqual(self.subset._items, self.rows) self.assertListEqual(list(self.subset), self.rows) def test_remove_row(self): """Test removing a row from the TableSubset.""" self.table.loaded = True [self.subset.add_row(row) for row in self.rows] expected = {row.number for row in self.rows} changed = unittest.mock.Mock() self.subset.connect("items-changed", changed) for n, i in enumerate([2, 0, 4, 1, 3], start=1): row = self.rows[i] rm = sorted(expected).index(i) with self.subTest(i=i, row=row.number): expected.discard(i) self.subset.remove_row(row) self.assertSetEqual(self.subset.keyset.keys, expected) self.assertEqual(self.subset.n_rows, 5 - n) changed.assert_called_with(self.subset, rm, 1, 0) self.assertEqual(self.subset.n_rows, 0) def test_contains(self): """Test the __contains__() magic method.""" self.table.loaded = True self.assertFalse(self.rows[0] in self.subset) self.subset.add_row(self.rows[0]) self.assertTrue(self.rows[0] in self.subset) def test_table_not_loaded(self): """Test operations when the table hasn't been loaded.""" self.subset.add_row(self.rows[0]) self.assertListEqual(self.subset._items, []) self.assertEqual(self.subset.n_rows, 0) self.assertIsNone(self.subset.get_item(0)) self.subset.remove_row(self.rows[0]) self.assertListEqual(self.subset._items, []) self.assertEqual(self.subset.n_rows, 0) def test_table_loaded(self): """Test changing the value of Table.loaded.""" changed = unittest.mock.Mock() self.subset.connect("items-changed", changed) self.table.loaded = True changed.assert_not_called() self.table.loaded = False changed.assert_not_called() self.subset.add_row(self.rows[0]) self.subset.add_row(self.rows[1]) self.table.loaded = True self.assertEqual(self.subset.n_rows, 2) changed.assert_called_with(self.subset, 0, 0, 2) self.table.loaded = False self.assertEqual(self.subset.n_rows, 0) changed.assert_called_with(self.subset, 0, 2, 0)