emmental/tests/db/test_table.py

591 lines
23 KiB
Python

# 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)