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