Compare commits
15 Commits
c195e68216
...
5e096fa704
Author | SHA1 | Date |
---|---|---|
Anna Schumaker | 5e096fa704 | |
Anna Schumaker | 6ebf29a632 | |
Anna Schumaker | a4f30d87e6 | |
Anna Schumaker | 51b290e1f0 | |
Anna Schumaker | fa203a72dd | |
Anna Schumaker | 3b8fb8531e | |
Anna Schumaker | 3e73ce0650 | |
Anna Schumaker | 17e4d85f1b | |
Anna Schumaker | 24675bf202 | |
Anna Schumaker | 072264a77c | |
Anna Schumaker | e7526f595f | |
Anna Schumaker | 7d2ec00da7 | |
Anna Schumaker | 70d7f5fa70 | |
Anna Schumaker | 2504f4b91d | |
Anna Schumaker | 7358183fef |
|
@ -20,8 +20,8 @@ from gi.repository import Gio
|
|||
from gi.repository import Adw
|
||||
|
||||
MAJOR_VERSION = 3
|
||||
MINOR_VERSION = 0
|
||||
MICRO_VERSION = 6
|
||||
MINOR_VERSION = 1
|
||||
MICRO_VERSION = 0
|
||||
|
||||
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
|
||||
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Copyright 2023 (c) Anna Schumaker.
|
||||
"""Functions for configuring a callback at a specific time."""
|
||||
import datetime
|
||||
import math
|
||||
from gi.repository import GLib
|
||||
|
||||
_GSOURCE_MAPPING = dict()
|
||||
_NEXT_ALARM_ID = 1
|
||||
|
||||
|
||||
def _calc_seconds(time: datetime.time) -> int:
|
||||
"""Calculate the number of seconds until the given time."""
|
||||
now = datetime.datetime.now()
|
||||
then = datetime.datetime.combine(now.date(), time)
|
||||
|
||||
if now >= then:
|
||||
then += datetime.timedelta(days=1)
|
||||
|
||||
return math.ceil((then - now).total_seconds())
|
||||
|
||||
|
||||
def __set_alarm(time: datetime.time, func: callable, alarm_id: int) -> None:
|
||||
gsrcid = GLib.timeout_add_seconds(_calc_seconds(time), _do_alarm,
|
||||
time, func, alarm_id)
|
||||
_GSOURCE_MAPPING[alarm_id] = gsrcid
|
||||
return alarm_id
|
||||
|
||||
|
||||
def _do_alarm(time: datetime.time, func: callable, alarm_id: int) -> bool:
|
||||
"""Run an alarm callback."""
|
||||
func()
|
||||
__set_alarm(time, func, alarm_id)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
|
||||
def set_alarm(time: datetime.time, func: callable) -> int:
|
||||
"""Register a callback to be called at a specific time."""
|
||||
global _NEXT_ALARM_ID
|
||||
|
||||
res = __set_alarm(time, func, _NEXT_ALARM_ID)
|
||||
_NEXT_ALARM_ID += 1
|
||||
return res
|
||||
|
||||
|
||||
def cancel_alarm(alarm_id: int) -> None:
|
||||
"""Cancel an alarm."""
|
||||
GLib.source_remove(_GSOURCE_MAPPING[alarm_id])
|
||||
del _GSOURCE_MAPPING[alarm_id]
|
|
@ -18,7 +18,8 @@ from . import tracks
|
|||
from . import years
|
||||
|
||||
|
||||
SQL_SCRIPT = pathlib.Path(__file__).parent / "emmental.sql"
|
||||
SQL_V1_SCRIPT = pathlib.Path(__file__).parent / "emmental.sql"
|
||||
SQL_V2_SCRIPT = pathlib.Path(__file__).parent / "upgrade-v2.sql"
|
||||
|
||||
|
||||
class Connection(connection.Connection):
|
||||
|
@ -47,9 +48,11 @@ class Connection(connection.Connection):
|
|||
user_version = self("PRAGMA user_version").fetchone()["user_version"]
|
||||
match user_version:
|
||||
case 0:
|
||||
with open(SQL_SCRIPT) as f:
|
||||
self._sql.executescript(f.read())
|
||||
case 1: pass
|
||||
self.executescript(SQL_V1_SCRIPT)
|
||||
self.executescript(SQL_V2_SCRIPT)
|
||||
case 1:
|
||||
self.executescript(SQL_V2_SCRIPT)
|
||||
case 2: pass
|
||||
case _:
|
||||
raise Exception(f"Unsupported data version: {user_version}")
|
||||
|
||||
|
|
|
@ -85,3 +85,11 @@ class Connection(GObject.GObject):
|
|||
return self._sql.executemany(statement, args)
|
||||
except sqlite3.InternalError:
|
||||
return None
|
||||
|
||||
def executescript(self, script: pathlib.Path) -> sqlite3.Cursor | None:
|
||||
"""Execute a SQL script."""
|
||||
if script.is_file():
|
||||
with open(script) as f:
|
||||
cur = self._sql.executescript(f.read())
|
||||
self.commit()
|
||||
return cur
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# Copyright 2022 (c) Anna Schumaker
|
||||
"""A custom Gio.ListModel for working with playlists."""
|
||||
import datetime
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from .. import alarm
|
||||
from . import playlist
|
||||
from . import tracks
|
||||
|
||||
|
@ -57,6 +59,11 @@ class Table(playlist.Table):
|
|||
def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs):
|
||||
"""Initialize the Playlists Table."""
|
||||
super().__init__(sql=sql, system_tracks=False, **kwargs)
|
||||
alarm.set_alarm(datetime.time(hour=0, minute=0, second=5),
|
||||
self.__at_midnight)
|
||||
|
||||
def __at_midnight(self) -> None:
|
||||
self.new_tracks.reload_tracks()
|
||||
|
||||
def __move_user_trackid(self, playlist: Playlist, trackid: int,
|
||||
*, offset: int) -> bool:
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/* Copyright 2023 (c) Anna Schumaker */
|
||||
|
||||
PRAGMA user_version = 2;
|
||||
|
||||
/*
|
||||
* The `saved_track_data` table is missing the date added field, which
|
||||
* causes restored tracks to show up in the "New Tracks" playlist again.
|
||||
* We can fix this by storing the date that the track was initially added
|
||||
* to the database, and restoring it later.
|
||||
*/
|
||||
|
||||
ALTER TABLE saved_track_data
|
||||
ADD COLUMN added DATE DEFAULT NULL;
|
||||
|
||||
UPDATE saved_track_data SET added = CURRENT_DATE;
|
||||
|
||||
DROP TRIGGER tracks_delete_save;
|
||||
CREATE TRIGGER tracks_delete_save BEFORE DELETE ON tracks
|
||||
WHEN OLD.mbid != "" BEGIN
|
||||
INSERT INTO saved_track_data
|
||||
(mbid, favorite, playcount, lastplayed, laststarted, added)
|
||||
VALUES (OLD.mbid, OLD.favorite, OLD.playcount,
|
||||
OLD.lastplayed, OLD.laststarted, OLD.added);
|
||||
END;
|
||||
|
||||
DROP TRIGGER tracks_insert_restore;
|
||||
CREATE TRIGGER tracks_insert_restore AFTER INSERT ON tracks
|
||||
WHEN NEW.mbid != "" BEGIN
|
||||
UPDATE tracks SET favorite = saved_track_data.favorite,
|
||||
playcount = saved_track_data.playcount,
|
||||
lastplayed = saved_track_data.lastplayed,
|
||||
laststarted = saved_track_data.laststarted,
|
||||
added = saved_track_data.added
|
||||
FROM saved_track_data
|
||||
WHERE tracks.mbid = saved_track_data.mbid AND
|
||||
tracks.mbid = NEW.mbid;
|
||||
DELETE FROM saved_track_data WHERE mbid = NEW.mbid;
|
||||
END;
|
|
@ -24,6 +24,9 @@ CSS_PRIORITY = gi.repository.Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
|||
CSS_PROVIDER = gi.repository.Gtk.CssProvider()
|
||||
CSS_PROVIDER.load_from_path(str(CSS_FILE))
|
||||
|
||||
CACHE_DIR = pathlib.Path(xdg.BaseDirectory.save_cache_path("emmental"))
|
||||
CACHE_DIR = CACHE_DIR / DEBUG_STR.lstrip("-")
|
||||
|
||||
DATA_DIR = pathlib.Path(xdg.BaseDirectory.save_data_path("emmental"))
|
||||
|
||||
RESOURCE_PATH = "/com/nowheycreamery/emmental"
|
||||
|
|
|
@ -6,7 +6,7 @@ Version = GLib.OptionEntry()
|
|||
Version.long_name = "version"
|
||||
Version.short_name = ord("v")
|
||||
Version.flags = GLib.OptionFlags.NONE
|
||||
Version.arg = GLib.OptionArg.NONE
|
||||
# Version.arg = GLib.OptionArg.NONE
|
||||
Version.arg_data = None
|
||||
Version.description = "Print version information and exit"
|
||||
Version.arg_description = None
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright 2022 (c) Anna Schumaker.
|
||||
"""A card for displaying the list of playlists."""
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
from . import artist
|
||||
from . import decade
|
||||
|
@ -66,27 +67,37 @@ class Card(Gtk.Box):
|
|||
if self.get_sensitive() is False:
|
||||
if False not in {tbl.loaded for tbl in sql.playlist_tables()}:
|
||||
self.set_sensitive(True)
|
||||
self.select_playlist(sql.active_playlist)
|
||||
self.select_playlist(sql.active_playlist, 150)
|
||||
if len(sql.libraries) == 0:
|
||||
self._libraries.extra_widget.emit("clicked")
|
||||
|
||||
def select_playlist(self, playlist: db.playlist.Playlist) -> None:
|
||||
"""Set the current active playlist."""
|
||||
def __select_playlist(self, playlist: db.playlist.Playlist) -> bool:
|
||||
if playlist is not None:
|
||||
match playlist.table:
|
||||
case self.sql.playlists:
|
||||
section = self._playlists
|
||||
case self.sql.artists | self.sql.albums | self.sql.media:
|
||||
section = self._artists
|
||||
case self.sql.genres:
|
||||
section = self._genres
|
||||
case self.sql.decades | self.sql.years:
|
||||
section = self._decades
|
||||
case self.sql.libraries:
|
||||
section = self._libraries
|
||||
|
||||
section.active = True
|
||||
section = self.table_section(playlist.table)
|
||||
if not section.active:
|
||||
section.active = True
|
||||
return GLib.SOURCE_CONTINUE
|
||||
section.select_playlist(playlist)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
def select_playlist(self, playlist: db.playlist.Playlist,
|
||||
timeout: int = 0) -> None:
|
||||
"""Set the current active playlist."""
|
||||
GLib.timeout_add(timeout, self.__select_playlist, playlist)
|
||||
|
||||
def table_section(self, table: db.playlist.Table) -> section.Section:
|
||||
"""Get the Section associated with a specific Playlist Table."""
|
||||
match table:
|
||||
case self.sql.playlists:
|
||||
return self._playlists
|
||||
case self.sql.artists | self.sql.albums | self.sql.media:
|
||||
return self._artists
|
||||
case self.sql.genres:
|
||||
return self._genres
|
||||
case self.sql.decades | self.sql.years:
|
||||
return self._decades
|
||||
case self.sql.libraries:
|
||||
return self._libraries
|
||||
|
||||
@property
|
||||
def accelerators(self) -> list[ActionEntry]:
|
||||
|
|
|
@ -4,9 +4,9 @@ import pathlib
|
|||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gio
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Adw
|
||||
from .. import texture
|
||||
|
||||
|
||||
IMAGE_FILTERS = Gio.ListStore()
|
||||
|
@ -37,11 +37,7 @@ class Icon(Adw.Bin):
|
|||
self.set_child(self._icon)
|
||||
|
||||
def __notify_filepath(self, icon: Adw.Bin, param) -> None:
|
||||
if self.filepath is None:
|
||||
texture = None
|
||||
else:
|
||||
texture = Gdk.Texture.new_from_filename(str(self.filepath))
|
||||
self._icon.set_custom_image(texture)
|
||||
self._icon.set_custom_image(texture.CACHE[self.filepath])
|
||||
|
||||
|
||||
class Settable(Icon):
|
||||
|
@ -61,7 +57,9 @@ class Settable(Icon):
|
|||
def __async_ready(self, dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||
try:
|
||||
file = dialog.open_finish(task)
|
||||
self.filepath = pathlib.Path(file.get_path())
|
||||
path = pathlib.Path(file.get_path())
|
||||
texture.CACHE.drop(path)
|
||||
self.filepath = path
|
||||
except GLib.Error:
|
||||
self.filepath = None
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"""A sidebar Header attached to a hidden ListView for selecting playlists."""
|
||||
import typing
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
from .. import db
|
||||
from .. import factory
|
||||
|
@ -86,9 +85,7 @@ class Section(header.Header):
|
|||
def select_playlist(self, playlist: db.playlist.Playlist) -> None:
|
||||
"""Select the requested playlist."""
|
||||
if (index := self.playlist_index(playlist)) is not None:
|
||||
self._selection.select_item(index, True)
|
||||
self._listview.activate_action("list.scroll-to-item",
|
||||
GLib.Variant.new_uint32(index))
|
||||
self._listview.scroll_to(index, Gtk.ListScrollFlags.SELECT)
|
||||
|
||||
@GObject.Signal(arg_types=(db.playlist.Playlist,))
|
||||
def playlist_activated(self, playlist: db.playlist.Playlist):
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
# Copyright 2023 (c) Anna Schumaker.
|
||||
"""A cache to hold Gdk.Textures used by cover art."""
|
||||
import pathlib
|
||||
import sys
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
if "unittest" in sys.modules:
|
||||
import tempfile
|
||||
TEMP_DIR = tempfile.TemporaryDirectory(prefix="emmental-")
|
||||
CACHE_PATH = pathlib.Path(TEMP_DIR.name)
|
||||
else:
|
||||
from . import gsetup
|
||||
CACHE_PATH = gsetup.CACHE_DIR
|
||||
|
||||
CACHE_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class _TextureCache(dict):
|
||||
"""A custom dictionary for storing texture files."""
|
||||
|
||||
def __check_update_cache(self, path: pathlib.Path) -> Gdk.Texture | None:
|
||||
if path.is_file() \
|
||||
and (cache_path := self.__get_cache_path(path)).exists() \
|
||||
and cache_path.stat().st_mtime < path.stat().st_mtime:
|
||||
self.__drop(path, cache_path)
|
||||
return self.__load_new_item(path, cache_path)
|
||||
|
||||
def __drop(self, path: pathlib.Path, cache_path: pathlib.Path) -> None:
|
||||
self.pop(path, None)
|
||||
cache_path.unlink(missing_ok=True)
|
||||
|
||||
def __get_cache_path(self, path: pathlib.Path) -> pathlib.Path:
|
||||
return CACHE_PATH / path.absolute().relative_to("/")
|
||||
|
||||
def __load_cached_item(self, path: pathlib.Path,
|
||||
cache_path: pathlib.Path) -> Gdk.Texture:
|
||||
texture = Gdk.Texture.new_from_filename(str(cache_path))
|
||||
self.__setitem__(path, texture)
|
||||
return texture
|
||||
|
||||
def __load_new_item(self, path: pathlib.Path,
|
||||
cache_path: pathlib.Path) -> Gdk.Texture:
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("rb") as f_path:
|
||||
bytes = f_path.read()
|
||||
with cache_path.open("wb") as f_cache:
|
||||
f_cache.write(bytes)
|
||||
texture = Gdk.Texture.new_from_bytes(GLib.Bytes.new(bytes))
|
||||
self.__setitem__(path, texture)
|
||||
return texture
|
||||
|
||||
def __get_missing_item(self, path: pathlib.Path,
|
||||
cache_path: pathlib.Path) -> Gdk.Texture:
|
||||
if cache_path.is_file():
|
||||
return self.__load_cached_item(path, cache_path)
|
||||
elif path.is_file():
|
||||
return self.__load_new_item(path, cache_path)
|
||||
|
||||
def __missing__(self, path: pathlib.Path | None) -> Gdk.Texture:
|
||||
"""Load a cache item from disk or add a new item entirely."""
|
||||
return self.__get_missing_item(path, self.__get_cache_path(path))
|
||||
|
||||
def __getitem__(self, path: pathlib.Path | None) -> Gdk.Texture | None:
|
||||
"""Get a Gdk.Texture cache item from the cache."""
|
||||
if path is not None:
|
||||
texture = self.__check_update_cache(path)
|
||||
return super().__getitem__(path) if texture is None else texture
|
||||
|
||||
def drop(self, path: pathlib.Path | None) -> None:
|
||||
"""Drop a single cache item from the cache."""
|
||||
self.__drop(path, self.__get_cache_path(path))
|
||||
|
||||
|
||||
CACHE = _TextureCache()
|
|
@ -4,10 +4,10 @@ import datetime
|
|||
import dateutil.tz
|
||||
import pathlib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from .. import buttons
|
||||
from .. import factory
|
||||
from .. import texture
|
||||
|
||||
|
||||
class TrackRow(factory.ListRow):
|
||||
|
@ -278,8 +278,6 @@ class MediumString(InscriptionRow):
|
|||
class AlbumCover(TrackRow):
|
||||
"""A Track Row to display Album art."""
|
||||
|
||||
Cache = dict()
|
||||
|
||||
filepath = GObject.Property(type=GObject.TYPE_PYOBJECT)
|
||||
|
||||
def __init__(self, listitem: Gtk.ListItem, property: str):
|
||||
|
@ -293,19 +291,14 @@ class AlbumCover(TrackRow):
|
|||
match param.name:
|
||||
case "mediumid": self.rebind_album("filepath", to_self=True)
|
||||
case "filepath":
|
||||
if self.filepath is None:
|
||||
texture = None
|
||||
elif (texture := AlbumCover.Cache.get(self.filepath)) is None:
|
||||
texture = Gdk.Texture.new_from_filename(str(self.filepath))
|
||||
AlbumCover.Cache[self.filepath] = texture
|
||||
|
||||
self.child.set_paintable(texture)
|
||||
self.child.set_has_tooltip(texture is not None)
|
||||
tex = texture.CACHE[self.filepath]
|
||||
self.child.set_paintable(tex)
|
||||
self.child.set_has_tooltip(tex is not None)
|
||||
|
||||
def __query_tooltip(self, child: Gtk.Picture, x: int, y: int,
|
||||
keyboard_mode: bool, tooltip: Gtk.Tooltip) -> bool:
|
||||
texture = AlbumCover.Cache.get(self.filepath)
|
||||
tooltip.set_custom(Gtk.Picture.new_for_paintable(texture))
|
||||
tex = texture.CACHE[self.filepath]
|
||||
tooltip.set_custom(Gtk.Picture.new_for_paintable(tex))
|
||||
return True
|
||||
|
||||
def do_bind(self) -> None:
|
||||
|
|
|
@ -81,13 +81,9 @@ class TrackView(Gtk.ScrolledWindow):
|
|||
|
||||
def scroll_to_track(self, track: db.tracks.Track) -> None:
|
||||
"""Scroll to the requested Track."""
|
||||
# This is a workaround until the ColumnView has better scrolling
|
||||
# support, which seems to be targeted for Gtk 4.10.
|
||||
adjustment = self._scrollwin.get_vadjustment()
|
||||
for (i, t) in enumerate(self._selection):
|
||||
if t == track:
|
||||
pos = max(i - 3, 0) * adjustment.get_upper()
|
||||
adjustment.set_value(pos / self._selection.get_n_items())
|
||||
for i in range(self._selection.props.n_items):
|
||||
if self._selection[i] == track:
|
||||
self._columnview.scroll_to(i, None, Gtk.ListScrollFlags.NONE)
|
||||
|
||||
@GObject.Property(type=Gio.ListModel)
|
||||
def columns(self) -> Gio.ListModel:
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/* Copyright 2023 (c) Anna Schumaker */
|
||||
CREATE TABLE test (a INT, b INT);
|
||||
INSERT INTO test VALUES (1, 2);
|
||||
INSERT INTO test VALUES (3, 4);
|
||||
INSERT INTO test VALUES (5, 6);
|
||||
INSERT INTO test VALUES (7, 8);
|
||||
INSERT INTO test VALUES (9, 0);
|
|
@ -79,6 +79,20 @@ class TestConnection(unittest.TestCase):
|
|||
self.assertEqual(tuple(rows[3]), (4, "d"))
|
||||
self.assertEqual(tuple(rows[4]), (5, "e"))
|
||||
|
||||
@unittest.mock.patch("emmental.db.connection.Connection.commit")
|
||||
def test_executescript(self, mock_commit: unittest.mock.Mock):
|
||||
"""Test the executescript function."""
|
||||
script = pathlib.Path(__file__).parent / "test-script.sql"
|
||||
cur = self.sql.executescript(script)
|
||||
self.assertIsInstance(cur, sqlite3.Cursor)
|
||||
mock_commit.assert_called()
|
||||
|
||||
rows = self.sql("SELECT * FROM test").fetchall()
|
||||
self.assertListEqual([(row["a"], row["b"]) for row in rows],
|
||||
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 0)])
|
||||
|
||||
self.assertIsNone(self.sql.executescript(script.parent / "no-script"))
|
||||
|
||||
def test_path_column(self):
|
||||
"""Test that the PATH column type has been set up."""
|
||||
self.sql("CREATE TABLE test (path PATH)")
|
||||
|
|
|
@ -11,8 +11,9 @@ class TestConnection(tests.util.TestCase):
|
|||
|
||||
def test_paths(self):
|
||||
"""Check that path constants are pointing to the right places."""
|
||||
script = pathlib.Path(emmental.db.__file__).parent / "emmental.sql"
|
||||
self.assertEqual(emmental.db.SQL_SCRIPT, script)
|
||||
dir = pathlib.Path(emmental.db.__file__).parent
|
||||
self.assertEqual(emmental.db.SQL_V1_SCRIPT, dir / "emmental.sql")
|
||||
self.assertEqual(emmental.db.SQL_V2_SCRIPT, dir / "upgrade-v2.sql")
|
||||
|
||||
def test_connection(self):
|
||||
"""Check that the connection manager is initialized properly."""
|
||||
|
@ -21,16 +22,16 @@ class TestConnection(tests.util.TestCase):
|
|||
def test_version(self):
|
||||
"""Test checking the database schema version."""
|
||||
cur = self.sql("PRAGMA user_version")
|
||||
self.assertEqual(cur.fetchone()["user_version"], 1)
|
||||
self.assertEqual(cur.fetchone()["user_version"], 2)
|
||||
|
||||
def test_version_too_new(self):
|
||||
"""Test failing when the database version is too new."""
|
||||
self.sql._Connection__check_version()
|
||||
|
||||
self.sql("PRAGMA user_version = 2")
|
||||
self.sql("PRAGMA user_version = 3")
|
||||
with self.assertRaises(Exception) as e:
|
||||
self.sql._Connection__check_version()
|
||||
self.assertEqual(str(e.exception), "Unsupported data version: 2")
|
||||
self.assertEqual(str(e.exception), "Unsupported data version: 3")
|
||||
|
||||
def test_close(self):
|
||||
"""Check closing the connection."""
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright 2022 (c) Anna Schumaker
|
||||
"""Tests our playlist Gio.ListModel."""
|
||||
import datetime
|
||||
import pathlib
|
||||
import unittest.mock
|
||||
import emmental.db
|
||||
|
@ -326,6 +327,18 @@ class TestSystemPlaylists(tests.util.TestCase):
|
|||
pathlib.Path("/a/b/1.ogg"),
|
||||
self.medium, self.year)
|
||||
|
||||
def test_midnight_alarm(self):
|
||||
"""Test playlist maintenance run every night at midnight."""
|
||||
with unittest.mock.patch.object(self.table.new_tracks,
|
||||
"reload_tracks") as mock_reload:
|
||||
self.table._Table__at_midnight()
|
||||
mock_reload.assert_called()
|
||||
|
||||
with unittest.mock.patch("emmental.alarm.set_alarm") as mock_set:
|
||||
table2 = emmental.db.playlists.Table(self.sql)
|
||||
mock_set.assert_called_with(datetime.time(second=5),
|
||||
table2._Table__at_midnight)
|
||||
|
||||
def test_collection(self):
|
||||
"""Test the Collection playlist."""
|
||||
self.assertIsInstance(self.table.collection,
|
||||
|
|
|
@ -248,9 +248,13 @@ class TestTrackTable(tests.util.TestCase):
|
|||
def test_create_restore(self):
|
||||
"""Test restoring saved track data."""
|
||||
now = datetime.datetime.now()
|
||||
today = datetime.date.today()
|
||||
yesterday = today - datetime.timedelta(days=1)
|
||||
self.sql("""INSERT INTO saved_track_data
|
||||
(mbid, favorite, playcount, lastplayed, laststarted)
|
||||
VALUES (?, ?, ?, ? , ?)""", "ab-cd-ef", True, 42, now, now)
|
||||
(mbid, favorite, playcount,
|
||||
lastplayed, laststarted, added)
|
||||
VALUES (?, ?, ?, ? , ?, ?)""",
|
||||
"ab-cd-ef", True, 42, now, now, yesterday)
|
||||
|
||||
track1 = self.tracks.create(self.library, pathlib.Path("/a/b/1.ogg"),
|
||||
self.medium, self.year)
|
||||
|
@ -258,6 +262,7 @@ class TestTrackTable(tests.util.TestCase):
|
|||
self.assertEqual(track1.playcount, 0)
|
||||
self.assertIsNone(track1.lastplayed)
|
||||
self.assertIsNone(track1.laststarted)
|
||||
self.assertEqual(track1.added, today)
|
||||
|
||||
row = self.sql("SELECT COUNT(*) FROM saved_track_data").fetchone()
|
||||
self.assertEqual(row["COUNT(*)"], 1)
|
||||
|
@ -268,6 +273,7 @@ class TestTrackTable(tests.util.TestCase):
|
|||
self.assertEqual(track2.playcount, 42)
|
||||
self.assertEqual(track2.lastplayed, now)
|
||||
self.assertEqual(track2.laststarted, now)
|
||||
self.assertEqual(track2.added, yesterday)
|
||||
|
||||
row = self.sql("SELECT COUNT(*) FROM saved_track_data").fetchone()
|
||||
self.assertEqual(row["COUNT(*)"], 0)
|
||||
|
@ -308,6 +314,7 @@ class TestTrackTable(tests.util.TestCase):
|
|||
self.assertEqual(rows[0]["laststarted"], now)
|
||||
self.assertEqual(rows[0]["lastplayed"], now)
|
||||
self.assertEqual(rows[0]["playcount"], 42)
|
||||
self.assertEqual(rows[0]["added"], datetime.date.today())
|
||||
|
||||
def test_filter(self):
|
||||
"""Test filtering the Track table."""
|
||||
|
|
|
@ -69,18 +69,14 @@ class TestIcon(unittest.TestCase):
|
|||
"""Test the filepath property."""
|
||||
self.assertIsNone(self.icon.filepath)
|
||||
|
||||
with unittest.mock.patch("gi.repository.Gdk.Texture.new_from_filename",
|
||||
wraps=Gdk.Texture.new_from_filename) \
|
||||
as mock_new:
|
||||
self.icon.filepath = tests.util.COVER_JPG
|
||||
mock_new.assert_called_with(str(tests.util.COVER_JPG))
|
||||
self.assertIsInstance(self.icon._icon.get_custom_image(),
|
||||
Gdk.Texture)
|
||||
self.icon.filepath = tests.util.COVER_JPG
|
||||
texture = self.icon._icon.get_custom_image()
|
||||
self.assertIsInstance(texture, Gdk.Texture)
|
||||
self.assertDictEqual(emmental.texture.CACHE,
|
||||
{tests.util.COVER_JPG: texture})
|
||||
|
||||
mock_new.reset_mock()
|
||||
self.icon.filepath = None
|
||||
self.assertIsNone(self.icon._icon.get_custom_image())
|
||||
mock_new.assert_not_called()
|
||||
self.icon.filepath = None
|
||||
self.assertIsNone(self.icon._icon.get_custom_image())
|
||||
|
||||
|
||||
class TestSettable(unittest.TestCase):
|
||||
|
@ -123,11 +119,15 @@ class TestSettable(unittest.TestCase):
|
|||
task = Gio.Task()
|
||||
cover_path = str(tests.util.COVER_JPG)
|
||||
mock_finish.return_value = Gio.File.new_for_path(cover_path)
|
||||
emmental.texture.CACHE[tests.util.COVER_JPG] = "abcde"
|
||||
|
||||
self.icon._Settable__async_ready(self.icon._dialog, task)
|
||||
mock_finish.assert_called_with(task)
|
||||
self.assertEqual(self.icon.filepath, tests.util.COVER_JPG)
|
||||
|
||||
texture = emmental.texture.CACHE[tests.util.COVER_JPG]
|
||||
self.assertIsInstance(texture, Gdk.Texture)
|
||||
|
||||
def test_clearing(self):
|
||||
"""Test clearing the icon by canceling the FileDialog."""
|
||||
mock_set_initial_file = unittest.mock.Mock()
|
||||
|
|
|
@ -4,7 +4,6 @@ import emmental.db
|
|||
import emmental.sidebar.section
|
||||
import tests.util
|
||||
import unittest.mock
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
|
@ -105,18 +104,12 @@ class TestSection(tests.util.TestCase):
|
|||
def test_select_playlist(self):
|
||||
"""Test selecting a specific playlist."""
|
||||
self.section.do_get_subtitle = unittest.mock.Mock(return_value="")
|
||||
|
||||
playlist_selected = unittest.mock.Mock()
|
||||
self.section.connect("playlist-selected", playlist_selected)
|
||||
playlist = self.table.create("Test Playlist")
|
||||
playlist_selected.assert_not_called()
|
||||
|
||||
with unittest.mock.patch.object(self.section._listview,
|
||||
"activate_action") as mock_action:
|
||||
"scroll_to") as mock_scroll_to:
|
||||
self.section.select_playlist(playlist)
|
||||
playlist_selected.assert_called_with(self.section, playlist)
|
||||
mock_action.assert_called_with("list.scroll-to-item",
|
||||
GLib.Variant.new_uint32(0))
|
||||
mock_scroll_to.assert_called_with(0, Gtk.ListScrollFlags.SELECT)
|
||||
|
||||
def test_playlist_selected(self):
|
||||
"""Test selecting a playlist in the list."""
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import emmental.sidebar
|
||||
import tests.util
|
||||
import unittest.mock
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
|
@ -73,10 +74,13 @@ class TestSidebar(tests.util.TestCase):
|
|||
self.assertFalse(self.sidebar.get_sensitive())
|
||||
self.sidebar.select_playlist.assert_not_called()
|
||||
self.sidebar._libraries.extra_widget.emit.assert_not_called()
|
||||
self.sql.emit("table-loaded", table)
|
||||
|
||||
self.assertTrue(self.sidebar.get_sensitive())
|
||||
self.sidebar.select_playlist.assert_called()
|
||||
table.load(now=True)
|
||||
self.assertEqual(self.sidebar.get_sensitive(),
|
||||
table == tables[-1])
|
||||
|
||||
playlist = self.sql.playlists.collection
|
||||
self.sidebar.select_playlist.assert_called_with(playlist, 150)
|
||||
self.sidebar._libraries.extra_widget.emit.assert_called_with("clicked")
|
||||
|
||||
self.sidebar.select_playlist.reset_mock()
|
||||
|
@ -147,45 +151,48 @@ class TestSidebar(tests.util.TestCase):
|
|||
|
||||
def test_select_playlist(self):
|
||||
"""Test setting the active playlist."""
|
||||
self.assertEqual(self.sidebar._Card__select_playlist(None),
|
||||
GLib.SOURCE_REMOVE)
|
||||
|
||||
playlist = self.sql.playlists.create("Test Playlist")
|
||||
self.sidebar.select_playlist(playlist)
|
||||
self.assertTrue(self.sidebar._playlists.active)
|
||||
self.assertEqual(self.sidebar.selected_playlist, playlist)
|
||||
with unittest.mock.patch.object(self.sidebar._playlists,
|
||||
"select_playlist") as mock_select:
|
||||
self.assertEqual(self.sidebar._Card__select_playlist(playlist),
|
||||
GLib.SOURCE_CONTINUE)
|
||||
self.assertTrue(self.sidebar._playlists.active)
|
||||
mock_select.assert_not_called()
|
||||
|
||||
artist = self.sql.artists.create("Test Artist")
|
||||
album = self.sql.albums.create("Test Album", "Test Artist", "2023")
|
||||
medium = self.sql.media.create(album, "Test Medium", number=1)
|
||||
self.assertEqual(self.sidebar._Card__select_playlist(playlist),
|
||||
GLib.SOURCE_REMOVE)
|
||||
mock_select.assert_called_with(playlist)
|
||||
|
||||
self.sidebar._artists.select_playlist = unittest.mock.Mock()
|
||||
for plist in [artist, album, medium]:
|
||||
self.sidebar._artists.select_playlist.reset_mock()
|
||||
self.sidebar._artists.active = False
|
||||
with unittest.mock.patch.object(GLib, "timeout_add") as mock_to:
|
||||
self.sidebar.select_playlist(playlist)
|
||||
mock_to.assert_called_with(0, self.sidebar._Card__select_playlist,
|
||||
playlist)
|
||||
self.sidebar.select_playlist(playlist, 42)
|
||||
mock_to.assert_called_with(42, self.sidebar._Card__select_playlist,
|
||||
playlist)
|
||||
|
||||
self.sidebar.select_playlist(plist)
|
||||
self.assertTrue(self.sidebar._artists.active)
|
||||
self.sidebar._artists.select_playlist.assert_called_with(plist)
|
||||
|
||||
genre = self.sql.genres.create("Test Genre")
|
||||
self.sidebar.select_playlist(genre)
|
||||
self.assertTrue(self.sidebar._genres.active)
|
||||
self.assertEqual(self.sidebar.selected_playlist, genre)
|
||||
|
||||
decade = self.sql.decades.create(1990)
|
||||
year = self.sql.years.create(1990)
|
||||
|
||||
self.sidebar._decades.select_playlist = unittest.mock.Mock()
|
||||
for plist in [decade, year]:
|
||||
self.sidebar._decades.select_playlist.reset_mock()
|
||||
self.sidebar._decades.active = False
|
||||
|
||||
self.sidebar.select_playlist(plist)
|
||||
self.assertTrue(self.sidebar._decades.active)
|
||||
self.sidebar._decades.select_playlist.assert_called_with(plist)
|
||||
|
||||
library = self.sql.libraries.create("/a/b/c")
|
||||
self.sidebar.select_playlist(library)
|
||||
self.assertTrue(self.sidebar._libraries.active)
|
||||
self.assertEqual(self.sidebar.selected_playlist, library)
|
||||
def test_table_section(self):
|
||||
"""Test converting a Playlist database table into a Section."""
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.playlists),
|
||||
self.sidebar._playlists)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.artists),
|
||||
self.sidebar._artists)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.albums),
|
||||
self.sidebar._artists)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.media),
|
||||
self.sidebar._artists)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.genres),
|
||||
self.sidebar._genres)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.decades),
|
||||
self.sidebar._decades)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.years),
|
||||
self.sidebar._decades)
|
||||
self.assertEqual(self.sidebar.table_section(self.sql.libraries),
|
||||
self.sidebar._libraries)
|
||||
self.assertIsNone(self.sidebar.table_section(None))
|
||||
|
||||
def test_accelerators(self):
|
||||
"""Check that the accelerators list is set up properly."""
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# Copyright 2023 (c) Anna Schumaker.
|
||||
"""Test our functions for callbacks at a specific time."""
|
||||
import datetime
|
||||
import unittest.mock
|
||||
import emmental.alarm
|
||||
from gi.repository import GLib
|
||||
|
||||
|
||||
class TestAlarm(unittest.TestCase):
|
||||
"""Test case for callbacks at a specific time."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
emmental.alarm._GSOURCE_MAPPING.clear()
|
||||
emmental.alarm._NEXT_ALARM_ID = 1
|
||||
self.midnight = datetime.time(hour=0, minute=0, second=0)
|
||||
|
||||
def test_state(self):
|
||||
"""Test our global state."""
|
||||
self.assertDictEqual(emmental.alarm._GSOURCE_MAPPING, {})
|
||||
self.assertEqual(emmental.alarm._NEXT_ALARM_ID, 1)
|
||||
|
||||
def test_calc_seconds(self):
|
||||
"""Test calculating the seconds until the next alarm."""
|
||||
now = datetime.datetime.now()
|
||||
time = (now + datetime.timedelta(minutes=2)).time()
|
||||
self.assertEqual(emmental.alarm._calc_seconds(time), 120)
|
||||
|
||||
time = (now - datetime.timedelta(minutes=2)).time()
|
||||
self.assertEqual(emmental.alarm._calc_seconds(time), 86280)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add_seconds")
|
||||
def test_set_alarm(self, mock_timeout_add: unittest.mock.Mock):
|
||||
"""Test setting an alarm."""
|
||||
callback = unittest.mock.Mock()
|
||||
seconds = emmental.alarm._calc_seconds(self.midnight)
|
||||
mock_timeout_add.return_value = 42
|
||||
|
||||
srcid = emmental.alarm.set_alarm(self.midnight, callback)
|
||||
mock_timeout_add.assert_called_with(seconds, emmental.alarm._do_alarm,
|
||||
self.midnight, callback, 1)
|
||||
self.assertEqual(srcid, 1)
|
||||
self.assertEqual(emmental.alarm._NEXT_ALARM_ID, 2)
|
||||
self.assertEqual(emmental.alarm._GSOURCE_MAPPING[1], 42)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.source_remove")
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add_seconds")
|
||||
def test_cancel_alarm(self, mock_timeout_add: unittest.mock.Mock,
|
||||
mock_source_remove: unittest.mock.Mock):
|
||||
"""Test cancelling an alarm."""
|
||||
callback = unittest.mock.Mock()
|
||||
mock_timeout_add.return_value = 42
|
||||
srcid = emmental.alarm.set_alarm(self.midnight, callback)
|
||||
|
||||
emmental.alarm.cancel_alarm(srcid)
|
||||
mock_source_remove.assert_called_with(42)
|
||||
self.assertNotIn(srcid, emmental.alarm._GSOURCE_MAPPING)
|
||||
|
||||
@unittest.mock.patch("gi.repository.GLib.timeout_add_seconds")
|
||||
def test_do_alarm(self, mock_timeout_add: unittest.mock.Mock):
|
||||
"""Test triggering an alarm."""
|
||||
callback = unittest.mock.Mock()
|
||||
seconds = emmental.alarm._calc_seconds(self.midnight)
|
||||
emmental.alarm._GSOURCE_MAPPING[1] = 2
|
||||
mock_timeout_add.return_value = 42
|
||||
|
||||
self.assertEqual(emmental.alarm._do_alarm(self.midnight, callback, 1),
|
||||
GLib.SOURCE_REMOVE)
|
||||
callback.assert_called()
|
||||
mock_timeout_add.assert_called_with(seconds, emmental.alarm._do_alarm,
|
||||
self.midnight, callback, 1)
|
||||
self.assertEqual(emmental.alarm._GSOURCE_MAPPING[1], 42)
|
|
@ -21,10 +21,10 @@ class TestEmmental(unittest.TestCase):
|
|||
def test_version(self):
|
||||
"""Check that version constants have been set properly."""
|
||||
self.assertEqual(emmental.MAJOR_VERSION, 3)
|
||||
self.assertEqual(emmental.MINOR_VERSION, 0)
|
||||
self.assertEqual(emmental.MICRO_VERSION, 6)
|
||||
self.assertEqual(emmental.VERSION_NUMBER, "3.0.6")
|
||||
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.6-debug")
|
||||
self.assertEqual(emmental.MINOR_VERSION, 1)
|
||||
self.assertEqual(emmental.MICRO_VERSION, 0)
|
||||
self.assertEqual(emmental.VERSION_NUMBER, "3.1.0")
|
||||
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.1.0-debug")
|
||||
|
||||
def test_application(self):
|
||||
"""Check that the application instance is initialized properly."""
|
||||
|
@ -63,7 +63,7 @@ class TestEmmental(unittest.TestCase):
|
|||
mock_startup.assert_called()
|
||||
mock_load.assert_called()
|
||||
mock_add_window.assert_called_with(self.application.win)
|
||||
mock_set_useragent.assert_called_with("emmental-debug", "3.0.6")
|
||||
mock_set_useragent.assert_called_with("emmental-debug", "3.1.0")
|
||||
|
||||
@unittest.mock.patch("sys.stdout")
|
||||
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
|
||||
|
|
|
@ -62,6 +62,12 @@ class TestGSetup(unittest.TestCase):
|
|||
self.assertIsInstance(emmental.gsetup.RESOURCE,
|
||||
gi.repository.Gio.Resource)
|
||||
|
||||
def test_cache_dir(self):
|
||||
"""Check that the CACHE_DIR points to the right place."""
|
||||
cache_path = xdg.BaseDirectory.save_cache_path("emmental")
|
||||
self.assertEqual(emmental.gsetup.CACHE_DIR,
|
||||
pathlib.Path(cache_path) / "debug")
|
||||
|
||||
def test_data_dir(self):
|
||||
"""Check that the DATA_DIR points to the right place."""
|
||||
data_path = xdg.BaseDirectory.save_data_path("emmental")
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright 2023 (c) Anna Schumaker.
|
||||
"""Tests our Gdk.Texture cache."""
|
||||
import emmental.texture
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import tests.util
|
||||
import unittest
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
class TestTextureCache(unittest.TestCase):
|
||||
"""Test our custom cache dictionary."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
cover = tests.util.COVER_JPG.absolute().relative_to("/")
|
||||
self.target = emmental.texture.CACHE_PATH / cover
|
||||
self.target2 = self.target.with_name("cover2.jpg")
|
||||
self.cache = emmental.texture._TextureCache()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up."""
|
||||
self.target2.unlink(missing_ok=True)
|
||||
(path := self.target).unlink(missing_ok=True)
|
||||
while (path := path.parent) != emmental.texture.CACHE_PATH:
|
||||
if path.is_dir():
|
||||
path.rmdir()
|
||||
|
||||
def test_path(self):
|
||||
"""Test the on-disk path of the texture cache."""
|
||||
self.assertIsInstance(emmental.texture.TEMP_DIR,
|
||||
tempfile.TemporaryDirectory)
|
||||
self.assertEqual(emmental.texture.CACHE_PATH,
|
||||
pathlib.Path(emmental.texture.TEMP_DIR.name))
|
||||
self.assertTrue(emmental.texture.CACHE_PATH.is_dir())
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the cache dict is initialized properly."""
|
||||
self.assertIsInstance(emmental.texture.CACHE,
|
||||
emmental.texture._TextureCache)
|
||||
self.assertDictEqual(emmental.texture.CACHE, {})
|
||||
|
||||
self.assertIsInstance(self.cache, dict)
|
||||
self.assertDictEqual(self.cache, {})
|
||||
|
||||
def test_drop(self):
|
||||
"""Test dropping items from the cache."""
|
||||
self.cache[tests.util.COVER_JPG]
|
||||
self.cache.drop(tests.util.COVER_JPG)
|
||||
self.assertDictEqual(self.cache, {})
|
||||
self.assertFalse(self.target.exists())
|
||||
|
||||
self.cache[tests.util.COVER_JPG]
|
||||
self.cache.clear()
|
||||
self.cache.drop(tests.util.COVER_JPG)
|
||||
self.assertFalse(self.target.exists())
|
||||
|
||||
def test_getitem(self):
|
||||
"""Test getting and creating items in the cache dict."""
|
||||
self.assertIsNone(self.cache[None])
|
||||
self.assertIsNone(self.cache[pathlib.Path("/no/such/path")])
|
||||
self.assertDictEqual(self.cache, {})
|
||||
self.assertListEqual(list(emmental.texture.CACHE_PATH.iterdir()), [])
|
||||
|
||||
texture = self.cache[tests.util.COVER_JPG]
|
||||
self.assertIsInstance(texture, Gdk.Texture)
|
||||
self.assertDictEqual(self.cache, {tests.util.COVER_JPG: texture})
|
||||
self.assertEqual(self.cache[tests.util.COVER_JPG], texture)
|
||||
self.assertTrue(self.target.is_file())
|
||||
|
||||
self.cache.clear()
|
||||
self.assertIsInstance(self.cache[tests.util.COVER_JPG], Gdk.Texture)
|
||||
|
||||
def test_getitem_cache_only(self):
|
||||
"""Test getting a cached item with deleted source path."""
|
||||
cover2 = tests.util.COVER_JPG.with_name("cover2.jpg")
|
||||
texture = self.cache[tests.util.COVER_JPG]
|
||||
self.cache[cover2] = texture
|
||||
del self.cache[tests.util.COVER_JPG]
|
||||
|
||||
self.assertEqual(self.cache[cover2], texture)
|
||||
|
||||
self.cache.clear()
|
||||
self.target.rename(self.target2)
|
||||
self.assertIsInstance(self.cache[cover2], Gdk.Texture)
|
||||
|
||||
def test_mtime_update(self):
|
||||
"""Test updating an item in the cache."""
|
||||
texture = self.cache[tests.util.COVER_JPG]
|
||||
os.utime(self.target, (123456789, 123456789))
|
||||
|
||||
new = self.cache[tests.util.COVER_JPG]
|
||||
self.assertIsInstance(new, Gdk.Texture)
|
||||
self.assertNotEqual(new, texture)
|
|
@ -8,7 +8,6 @@ import emmental.tracklist.row
|
|||
import tests.util
|
||||
import unittest.mock
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Adw
|
||||
|
||||
|
@ -193,9 +192,6 @@ class TestTrackRowWidgets(tests.util.TestCase):
|
|||
|
||||
def test_album_cover(self):
|
||||
"""Test the Album Cover widget."""
|
||||
self.assertDictEqual(emmental.tracklist.row.AlbumCover.Cache, {})
|
||||
cache = emmental.tracklist.row.AlbumCover.Cache
|
||||
|
||||
row = emmental.tracklist.row.AlbumCover(self.listitem, "cover")
|
||||
self.assertIsInstance(row, emmental.tracklist.row.TrackRow)
|
||||
self.assertIsInstance(row.child, Gtk.Picture)
|
||||
|
@ -206,10 +202,9 @@ class TestTrackRowWidgets(tests.util.TestCase):
|
|||
row.bind()
|
||||
self.assertEqual(row.filepath, tests.util.COVER_JPG)
|
||||
|
||||
self.assertEqual(len(cache), 1)
|
||||
self.assertIsInstance(cache[tests.util.COVER_JPG], Gdk.Texture)
|
||||
self.assertEqual(len(emmental.texture.CACHE), 1)
|
||||
self.assertEqual(row.child.get_paintable(),
|
||||
cache[tests.util.COVER_JPG])
|
||||
emmental.texture.CACHE[tests.util.COVER_JPG])
|
||||
self.assertTrue(row.child.get_has_tooltip())
|
||||
|
||||
self.album.cover = None
|
||||
|
|
Loading…
Reference in New Issue