emmental/tests/db/test_libraries.py

416 lines
18 KiB
Python

# Copyright 2022 (c) Anna Schumaker
"""Tests our library Gio.ListModel."""
import pathlib
import emmental.db
import tests.util
import unittest.mock
class TestLibraryObject(tests.util.TestCase):
"""Tests our library object."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.table = self.sql.libraries
self.path = pathlib.Path("/a/b/c")
self.library = emmental.db.libraries.Library(table=self.table,
libraryid=123,
propertyid=456,
path=self.path,
name=str(self.path))
def tearDown(self):
"""Clean up."""
super().tearDown()
self.library.stop()
def test_init(self):
"""Test that the Library is set up properly."""
self.assertIsInstance(self.library, emmental.db.playlist.Playlist)
self.assertIsInstance(self.library.queue, emmental.db.idle.Queue)
self.assertIsNone(self.library.readdir)
self.assertIsNone(self.library.tagger)
self.assertEqual(self.library.table, self.table)
self.assertEqual(self.library.propertyid, 456)
self.assertEqual(self.library.libraryid, 123)
self.assertEqual(self.library.primary_key, 123)
self.assertEqual(self.library.path, self.path)
self.assertTrue(self.library.enabled)
self.assertFalse(self.library.deleting)
self.assertIsNone(self.library.parent)
self.assertFalse(self.library.online)
self.assertEqual(self.library.queue[0],
(self.library._Library__scan_library,))
def test_delete(self):
"""Test deleting a Library path."""
self.assertFalse(self.library.deleting)
artist = self.sql.artists.create("Test Artist")
album = self.sql.albums.create("Test Album", "Test Artist", "2023")
medium = self.sql.media.create(album, "", number=1)
genre = self.sql.genres.create("Test Genre")
decade = self.sql.decades.create(1980)
year = self.sql.years.create(1988)
playlists = [plist for plist in self.sql.playlists.store] + \
[artist, album, medium, genre, decade, year]
for playlist in playlists:
playlist.reload_tracks = unittest.mock.Mock()
self.table.delete = unittest.mock.Mock()
self.sql.tracks.clear = unittest.mock.Mock()
self.sql.tracks.load = unittest.mock.Mock()
with unittest.mock.patch.object(self.table, "delete") as mock_delete:
with unittest.mock.patch.object(self.table, "update"):
self.assertTrue(self.library.delete())
self.assertTrue(self.library.deleting)
mock_delete.assert_not_called()
tasks = [(self.library._Library__reload_playlist_tracks, plist)
for plist in playlists]
tasks.append((self.library._Library__queue_delete,))
self.assertListEqual(self.library.queue._tasks, tasks)
self.sql.tracks.clear.assert_called()
self.sql.tracks.load.assert_not_called()
self.library.queue.cancel()
self.assertFalse(self.library.delete())
self.assertListEqual(self.library.queue._tasks, [])
self.library.deleting = False
self.assertTrue(self.library.delete())
self.library.queue.complete()
for plist in playlists:
plist.reload_tracks.assert_called_with(idle=False)
mock_delete.assert_called_with(self.library)
self.sql.tracks.load.assert_called()
def test_online(self):
"""Test that changing the online property notifies the table."""
with unittest.mock.patch.object(self.table,
"notify_online") as mock_notify:
self.library.online = True
mock_notify.assert_called_with(self.library)
@unittest.mock.patch.object(pathlib.Path, "iterdir", return_value=[])
@unittest.mock.patch.object(pathlib.Path, "is_dir")
def test_scan_online(self, mock_is_dir: unittest.mock.Mock,
mock_iterdir: unittest.mock.Mock):
"""Test scanning an online Library path."""
self.assertEqual(self.library.queue[0],
(self.library._Library__scan_library,))
self.library.tracks.trackids = {1, 2, 3}
self.library.load_tracks = unittest.mock.Mock()
with unittest.mock.patch.object(self.library.queue,
"push") as mock_push:
self.library.scan()
mock_push.assert_not_called()
mock_is_dir.return_value = True
self.assertTrue(self.library.queue.run_task())
self.library.load_tracks.assert_called()
mock_is_dir.assert_called()
self.assertTrue(self.library.online)
self.assertTrue(self.library.queue.running)
self.assertIsInstance(self.library.readdir,
emmental.path.ReaddirThread)
self.assertIsInstance(self.library.tagger,
emmental.db.tagger.Thread)
self.assertEqual(self.library.queue._tasks,
[(self.library._Library__check_trackid, 1),
(self.library._Library__check_trackid, 2),
(self.library._Library__check_trackid, 3),
(self.library._Library__queue_tracks,)])
def test_scan_offline(self):
"""Test scanning an offline Library path."""
self.assertEqual(self.library.queue[0],
(self.library._Library__scan_library,))
self.assertFalse(self.library.queue.run_task())
self.assertFalse(self.library.queue.running)
self.assertFalse(self.library.online)
self.assertIsNone(self.library.readdir)
@unittest.mock.patch.object(pathlib.Path, "iterdir", return_value=[])
@unittest.mock.patch.object(pathlib.Path, "is_file", return_value=True)
def test_scan_queue_tracks(self, mock_is_file: unittest.mock.Mock,
mock_iterdir: unittest.mock.Mock):
"""Test that tracks are queued for scanning."""
self.library.stop()
self.library.readdir = unittest.mock.Mock()
readdir = self.library.readdir
readdir.poll_result.return_value = []
self.assertFalse(self.library._Library__queue_tracks())
readdir.poll_result.assert_called()
readdir.stop.assert_not_called()
tag_func = self.library._Library__tag_track
readdir.poll_result.return_value = [pathlib.Path("/a/b/c/1.ogg"),
pathlib.Path("/a/b/c/2.ogg")]
self.assertFalse(self.library._Library__queue_tracks())
self.assertListEqual(self.library.queue._tasks,
[(tag_func, pathlib.Path("/a/b/c/1.ogg")),
(tag_func, pathlib.Path("/a/b/c/2.ogg"))])
readdir.poll_result.return_value = None
self.assertTrue(self.library._Library__queue_tracks())
self.assertListEqual(self.library.queue._tasks,
[(tag_func, pathlib.Path("/a/b/c/1.ogg")),
(tag_func, pathlib.Path("/a/b/c/2.ogg")),
(self.library._Library__stop_thread, "tagger")])
readdir.stop.assert_called()
def test_scan_tag_track(self):
"""Test that tracks are tagged during scanning."""
track = pathlib.Path("/a/b/c/1.ogg")
raw_tags = emmental.audio.tagger._Tags(track, {})
tags = emmental.db.tagger.Tags(self.sql, raw_tags, self.library)
tagger = unittest.mock.Mock()
self.library.tagger = tagger
tagger.ready.is_set.return_value = False
self.assertFalse(self.library._Library__tag_track(track))
tagger.get_result.assert_not_called()
tagger.tag_file.assert_not_called()
tagger.ready.is_set.return_value = True
tagger.get_result.return_value = None
self.assertFalse(self.library._Library__tag_track(track))
tagger.get_result.assert_called_with(db=self.sql, library=self.library)
tagger.tag_file.assert_called_with(track, mtime=None)
self.sql.tracks.lookup = unittest.mock.Mock()
self.sql.tracks.lookup.return_value.mtime = 12345
self.assertFalse(self.library._Library__tag_track(track))
tagger.get_result.assert_called_with(db=self.sql, library=self.library)
tagger.tag_file.assert_called_with(track, mtime=12345)
tagger.reset_mock()
tagger.ready.is_set.return_value = True
tagger.get_result.return_value = {"path": track, "tags": tags}
self.assertTrue(self.library._Library__tag_track(track))
tagger.tag_file.assert_not_called()
tagger.get_result.assert_called_with(db=self.sql, library=self.library)
@unittest.mock.patch("emmental.db.tagger.untag_track")
def test_scan_check_trackid(self, mock_untag: unittest.mock.Mock()):
"""Test that deleted Tracks are removed during scanning."""
track = unittest.mock.Mock()
track.path = pathlib.Path("/a/b/c/1.ogg")
self.library._Library__check_trackid(1)
mock_untag.assert_not_called()
self.sql.tracks.rows[1] = track
with unittest.mock.patch.object(type(track.path),
"exists") as mock_exists:
mock_exists.return_value = True
self.library._Library__check_trackid(1)
mock_untag.assert_not_called()
track.delete.assert_not_called()
self.library._Library__check_trackid(1)
mock_untag.assert_called_with(self.sql, track)
track.delete.assert_called()
def test_stop(self):
"""Test stopping a Library's background work."""
readdir = unittest.mock.Mock()
tagger = unittest.mock.Mock()
self.library.readdir = readdir
self.library.tagger = tagger
self.library.queue.running = True
self.library.stop()
self.assertIsNone(self.library.readdir)
self.assertIsNone(self.library.tagger)
self.assertFalse(self.library.queue.running)
readdir.stop.assert_called()
tagger.stop.assert_called()
class TestLibraryTable(tests.util.TestCase):
"""Tests our library table."""
def setUp(self):
"""Set up common variables."""
tests.util.TestCase.setUp(self)
self.table = self.sql.libraries
self.album = self.sql.albums.create("Test Album", "Test Artist", "123")
self.medium = self.sql.media.create(self.album, "", number=1)
self.year = self.sql.years.create(2023)
def test_init(self):
"""Test that the library model is configured correctly."""
self.assertIsInstance(self.table, emmental.db.playlist.Table)
self.assertEqual(len(self.table), 0)
self.assertFalse(self.table.autodelete)
self.assertFalse(self.table.system_tracks)
def test_add_track(self):
"""Test adding a Track to a Library playlist."""
library = self.table.create(pathlib.Path("/a/b"))
track = self.sql.tracks.create(library, pathlib.Path("/a/b/c.ogg"),
self.medium, self.year)
self.assertTrue(self.table.add_track(library, track))
library2 = self.table.create(pathlib.Path("/a/d"))
self.assertFalse(self.table.add_track(library2, track))
def test_construct(self):
"""Test constructing a new library."""
library = self.table.construct(propertyid=1, libraryid=1,
path=pathlib.Path("/a/b/c"),
name="/a/b/c")
self.assertIsInstance(library, emmental.db.libraries.Library)
self.assertEqual(library.table, self.table)
self.assertEqual(library.propertyid, 1)
self.assertEqual(library.libraryid, 1)
self.assertEqual(library.path, pathlib.Path("/a/b/c"))
self.assertEqual(library.name, "/a/b/c")
self.assertFalse(library.active)
self.assertFalse(library.tracks_movable)
def test_create(self):
"""Test creating a library."""
library = self.table.create(pathlib.Path("/a/b/c"))
self.assertIsInstance(library, emmental.db.libraries.Library)
self.assertEqual(library.path, pathlib.Path("/a/b/c"))
self.assertEqual(library.sort_order, "filepath")
cur = self.sql("SELECT COUNT(path) FROM libraries")
self.assertEqual(cur.fetchone()["COUNT(path)"], 1)
self.assertEqual(len(self.table.store), 1)
self.assertEqual(self.table.store.get_item(0), library)
cur = self.sql("""SELECT COUNT(*) FROM playlist_properties
WHERE propertyid=?""", library.propertyid)
self.assertEqual(cur.fetchone()["COUNT(*)"], 1)
self.assertIsNone(self.table.create("/a/b/c"))
def test_delete(self):
"""Test deleting a library."""
library = self.table.create(pathlib.Path("/a/b/c"))
self.assertTrue(self.table.delete(library))
self.assertIsNone(self.table.index(library))
cur = self.sql("SELECT COUNT(path) FROM libraries")
self.assertEqual(cur.fetchone()["COUNT(path)"], 0)
self.assertEqual(len(self.table), 0)
self.assertIsNone(self.table.get_item(0))
cur = self.sql("""SELECT COUNT(*) FROM playlist_properties
WHERE propertyid=?""", library.propertyid)
self.assertEqual(cur.fetchone()["COUNT(*)"], 0)
self.assertFalse(self.table.delete(library))
def test_filter(self):
"""Test filtering the library model."""
self.table.create(pathlib.Path("/a/b/c"))
self.table.create(pathlib.Path("/a/b/d"))
self.table.filter("*c", now=True)
self.assertSetEqual(self.table.get_filter().keys, {1})
self.table.filter("*a/b*", now=True)
self.assertSetEqual(self.table.get_filter().keys, {1, 2})
def test_get_trackids(self):
"""Test loading library tracks from the database."""
library = self.table.create("/a/b/")
track1 = self.sql.tracks.create(library, pathlib.Path("/a/b/1.ogg"),
self.medium, self.year)
track2 = self.sql.tracks.create(library, pathlib.Path("/a/b/2.ogg"),
self.medium, self.year)
self.assertSetEqual(self.table.get_trackids(library),
{track1.trackid, track2.trackid})
def test_load(self):
"""Test loading libraries from the database."""
self.table.create("/a/b/c")
self.table.create("/a/b/d").enabled = False
libraries2 = emmental.db.libraries.Table(self.sql)
self.assertEqual(len(libraries2), 0)
libraries2.load(now=True)
self.assertEqual(len(libraries2), 2)
self.assertEqual(libraries2.get_item(0).libraryid, 1)
self.assertEqual(libraries2.get_item(0).path, pathlib.Path("/a/b/c"))
self.assertTrue(libraries2.get_item(0).enabled)
self.assertEqual(libraries2.get_item(1).libraryid, 2)
self.assertEqual(libraries2.get_item(1).path, pathlib.Path("/a/b/d"))
self.assertFalse(libraries2.get_item(1).enabled)
def test_lookup(self):
"""Test looking up a library."""
library = self.table.create(pathlib.Path("/a/b/c"))
self.assertEqual(self.table.lookup(pathlib.Path("/a/b/c/")), library)
self.assertIsNone(self.table.lookup(pathlib.Path("/no/library/path")))
def test_remove_track(self):
"""Test removing a Track from the Library."""
library = self.table.create("/a/b/")
self.assertTrue(self.table.remove_track(library, unittest.mock.Mock()))
def test_stop(self):
"""Test stopping the library table."""
library = self.table.create(pathlib.Path("/a/b/c"))
library.queue.running = True
self.table.stop()
self.assertFalse(self.table.queue.running)
self.assertFalse(library.queue.running)
def test_update(self):
"""Test updating genre attributes."""
library = self.table.create("/a/b/c")
library.active = True
library.enabled = False
library.loop = "Track"
library.shuffle = True
library.sort_order = "trackid"
row = self.sql("""SELECT active, enabled, loop, shuffle,
sort_order, current_trackid
FROM libraries_view WHERE libraryid=?""",
library.libraryid).fetchone()
self.assertTrue(row["active"])
self.assertFalse(row["enabled"])
self.assertEqual(row["loop"], "Track")
self.assertTrue(row["shuffle"])
self.assertEqual(row["sort_order"], "trackid")
self.assertIsNone(row["current_trackid"])
def test_library_online(self):
"""Test the library-online signal."""
library = self.table.create(pathlib.Path("/a/b/c"))
callback = unittest.mock.Mock()
self.table.connect("library-online", callback)
library.online = True
callback.assert_not_called()
library.online = False
callback.assert_called_with(self.table, library)
callback.reset_mock()
self.table.loaded = True
library.online = True
callback.assert_called_with(self.table, library)
callback.reset_mock()
library.online = False
callback.assert_called_with(self.table, library)