Added tests for the filesystem adapter
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
@@ -250,10 +251,14 @@ class CachingAdapter(Adapter):
|
||||
|
||||
# Data Ingestion Methods
|
||||
# =========================================================================
|
||||
class FunctionNames(Enum):
|
||||
GET_PLAYLISTS = 'get_playlists'
|
||||
GET_PLAYLIST_DETAILS = 'get_playlist_details'
|
||||
|
||||
@abc.abstractmethod
|
||||
def ingest_new_data(
|
||||
self,
|
||||
function_name: str,
|
||||
function: 'CachingAdapter.FunctionNames',
|
||||
params: Tuple[Any, ...],
|
||||
data: Any,
|
||||
):
|
||||
|
@@ -202,7 +202,7 @@ class AdapterManager:
|
||||
assert AdapterManager._instance
|
||||
assert AdapterManager._instance.caching_adapter
|
||||
AdapterManager._instance.caching_adapter.ingest_new_data(
|
||||
'get_playlists',
|
||||
CachingAdapter.FunctionNames.GET_PLAYLISTS,
|
||||
(),
|
||||
f.result(),
|
||||
)
|
||||
@@ -265,7 +265,7 @@ class AdapterManager:
|
||||
assert AdapterManager._instance
|
||||
assert AdapterManager._instance.caching_adapter
|
||||
AdapterManager._instance.caching_adapter.ingest_new_data(
|
||||
'get_playlist_details',
|
||||
CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
|
||||
(playlist_id, ),
|
||||
f.result(),
|
||||
)
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import logging
|
||||
from dataclasses import asdict
|
||||
import threading
|
||||
from dataclasses import asdict, fields
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Sequence, Optional, Tuple
|
||||
from queue import PriorityQueue
|
||||
from time import sleep
|
||||
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from typing import Any, Dict, Optional, Sequence, Tuple
|
||||
|
||||
from sublime.adapters.api_objects import (Playlist, PlaylistDetails)
|
||||
|
||||
@@ -16,7 +17,6 @@ class FilesystemAdapter(CachingAdapter):
|
||||
"""
|
||||
Defines an adapter which retrieves its data from the local filesystem.
|
||||
"""
|
||||
|
||||
# Configuration and Initialization Properties
|
||||
# =========================================================================
|
||||
@staticmethod
|
||||
@@ -35,14 +35,11 @@ class FilesystemAdapter(CachingAdapter):
|
||||
is_cache: bool = False,
|
||||
):
|
||||
self.data_directory = data_directory
|
||||
logging.info('Opening connection to the database.')
|
||||
self.is_cache = is_cache
|
||||
database_filename = data_directory.joinpath('cache.db')
|
||||
models.database.initialize(
|
||||
SqliteQueueDatabase(database_filename, autorollback=True))
|
||||
models.database.init(database_filename)
|
||||
models.database.connect()
|
||||
models.database.create_tables(models.ALL_TABLES)
|
||||
sleep(1)
|
||||
assert len(models.database.get_tables()) > 0
|
||||
|
||||
def shutdown(self):
|
||||
logging.info('Shutdown complete')
|
||||
@@ -62,8 +59,13 @@ class FilesystemAdapter(CachingAdapter):
|
||||
|
||||
def get_playlists(self) -> Sequence[Playlist]:
|
||||
playlists = list(models.Playlist.select())
|
||||
if len(playlists) == 0: # TODO not necessarily a cache miss
|
||||
raise CacheMissError()
|
||||
if self.is_cache and len(playlists) == 0:
|
||||
# Determine if the adapter has ingested data for get_playlists
|
||||
# before. If not, cache miss.
|
||||
function_name = CachingAdapter.FunctionNames.GET_PLAYLISTS
|
||||
if not models.CacheInfo.get_or_none(
|
||||
models.CacheInfo.query_name == function_name):
|
||||
raise CacheMissError()
|
||||
return playlists
|
||||
|
||||
can_get_playlist_details: bool = True
|
||||
@@ -72,18 +74,38 @@ class FilesystemAdapter(CachingAdapter):
|
||||
self,
|
||||
playlist_id: str,
|
||||
) -> PlaylistDetails:
|
||||
raise NotImplementedError()
|
||||
playlist = models.Playlist.get_or_none(
|
||||
models.Playlist.id == playlist_id)
|
||||
if not playlist:
|
||||
if self.is_cache:
|
||||
raise CacheMissError()
|
||||
else:
|
||||
raise Exception(f'Playlist {playlist_id} does not exist.')
|
||||
|
||||
return playlist
|
||||
|
||||
# Data Ingestion Methods
|
||||
# =========================================================================
|
||||
def ingest_new_data(
|
||||
self,
|
||||
function_name: str,
|
||||
function: 'CachingAdapter.FunctionNames',
|
||||
params: Tuple[Any, ...],
|
||||
data: Any,
|
||||
):
|
||||
if function_name == 'get_playlists':
|
||||
(
|
||||
models.Playlist.insert_many(
|
||||
map(lambda p: models.Playlist(**asdict(p)),
|
||||
data)).on_conflict_replace())
|
||||
if not self.is_cache:
|
||||
raise Exception('FilesystemAdapter is not in cache mode')
|
||||
|
||||
models.CacheInfo.insert(
|
||||
query_name=function,
|
||||
last_ingestion_time=datetime.now(),
|
||||
).on_conflict_replace().execute()
|
||||
|
||||
if function == CachingAdapter.FunctionNames.GET_PLAYLISTS:
|
||||
models.Playlist.insert_many(map(
|
||||
asdict, data)).on_conflict_replace().execute()
|
||||
elif function == CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS:
|
||||
playlist_data = asdict(data)
|
||||
# TODO deal with the songs
|
||||
del playlist_data['songs']
|
||||
models.Playlist.insert(
|
||||
playlist_data).on_conflict_replace().execute()
|
||||
|
@@ -1,25 +1,27 @@
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Optional
|
||||
|
||||
from peewee import (
|
||||
BooleanField,
|
||||
ManyToManyField,
|
||||
DateTimeField,
|
||||
DatabaseProxy,
|
||||
Field,
|
||||
SqliteDatabase,
|
||||
ForeignKeyField,
|
||||
IntegerField,
|
||||
Model,
|
||||
TextField,
|
||||
)
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
database = DatabaseProxy()
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
from sublime.adapters.adapter_base import CachingAdapter
|
||||
|
||||
database = SqliteDatabase(None)
|
||||
|
||||
|
||||
# Custom Fields
|
||||
# =============================================================================
|
||||
class DurationField(IntegerField):
|
||||
def db_value(self, value: timedelta) -> Optional[int]:
|
||||
return value.microseconds if value else None
|
||||
@@ -28,24 +30,55 @@ class DurationField(IntegerField):
|
||||
return timedelta(microseconds=value) if value else None
|
||||
|
||||
|
||||
class CacheConstantsField(TextField):
|
||||
def db_value(self, value: CachingAdapter.FunctionNames) -> str:
|
||||
return value.value
|
||||
|
||||
def python_value(self, value: str) -> CachingAdapter.FunctionNames:
|
||||
return CachingAdapter.FunctionNames(value)
|
||||
|
||||
|
||||
# Models
|
||||
# =============================================================================
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
|
||||
class CoverArt(BaseModel):
|
||||
id = TextField(unique=True, primary_key=True)
|
||||
url = TextField()
|
||||
filename = TextField(null=True)
|
||||
|
||||
|
||||
class Song(BaseModel):
|
||||
id = TextField(unique=True, primary_key=True)
|
||||
|
||||
|
||||
class CacheInfo(BaseModel):
|
||||
query_name = CacheConstantsField(unique=True, primary_key=True)
|
||||
last_ingestion_time = DateTimeField(null=False)
|
||||
|
||||
|
||||
class Playlist(BaseModel):
|
||||
id = TextField(unique=True, primary_key=True)
|
||||
name = TextField()
|
||||
song_count = IntegerField()
|
||||
duration = DurationField()
|
||||
created = DateTimeField()
|
||||
changed = DateTimeField()
|
||||
public = BooleanField()
|
||||
cover_art = TextField()
|
||||
comment = TextField(null=True)
|
||||
owner = TextField(null=True)
|
||||
song_count = IntegerField(null=True)
|
||||
duration = DurationField(null=True)
|
||||
created = DateTimeField(null=True)
|
||||
changed = DateTimeField(null=True)
|
||||
public = BooleanField(null=True)
|
||||
cover_art = TextField(null=True)
|
||||
|
||||
songs = ManyToManyField(Song, backref='playlists')
|
||||
|
||||
|
||||
ALL_TABLES = (
|
||||
CacheInfo,
|
||||
CoverArt,
|
||||
Playlist,
|
||||
Playlist.songs.get_through_model(),
|
||||
Song,
|
||||
)
|
||||
|
@@ -3,7 +3,7 @@ import pytest
|
||||
from sublime.adapters import Adapter, AdapterManager
|
||||
|
||||
|
||||
class TestAdapter(Adapter):
|
||||
class MyAdapter(Adapter):
|
||||
pass
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from time import sleep
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -7,6 +8,8 @@ from typing import Any, Dict, Generator, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from sublime.adapters import CacheMissError
|
||||
from sublime.adapters.subsonic import api_objects as SubsonicAPI
|
||||
from sublime.adapters.filesystem import (
|
||||
models,
|
||||
FilesystemAdapter,
|
||||
@@ -43,5 +46,79 @@ def mock_data_files(
|
||||
yield file, f.read()
|
||||
|
||||
|
||||
def test_get_playlists(adapter: FilesystemAdapter, tmp_path: Path):
|
||||
assert adapter.get_playlists() == []
|
||||
def test_caching_get_playlists(
|
||||
cache_adapter: FilesystemAdapter,
|
||||
tmp_path: Path,
|
||||
):
|
||||
with pytest.raises(CacheMissError):
|
||||
cache_adapter.get_playlists()
|
||||
|
||||
# Ingest an empty list (for example, no playlists added yet to server).
|
||||
cache_adapter.ingest_new_data(
|
||||
FilesystemAdapter.FunctionNames.GET_PLAYLISTS, (), [])
|
||||
|
||||
# After the first cache miss of get_playlists, even if an empty list is
|
||||
# returned, the next one should not be a cache miss.
|
||||
cache_adapter.get_playlists()
|
||||
|
||||
# Ingest two playlists.
|
||||
cache_adapter.ingest_new_data(
|
||||
FilesystemAdapter.FunctionNames.GET_PLAYLISTS,
|
||||
(),
|
||||
[
|
||||
SubsonicAPI.Playlist('1', 'test1', comment='comment'),
|
||||
SubsonicAPI.Playlist('2', 'test2'),
|
||||
],
|
||||
)
|
||||
|
||||
playlists = cache_adapter.get_playlists()
|
||||
assert len(playlists) == 2
|
||||
assert (playlists[0].id, playlists[0].name,
|
||||
playlists[0].comment) == ('1', 'test1', 'comment')
|
||||
assert (playlists[1].id, playlists[1].name) == ('2', 'test2')
|
||||
|
||||
|
||||
def test_no_caching_get_playlists(adapter: FilesystemAdapter, tmp_path: Path):
|
||||
adapter.get_playlists()
|
||||
|
||||
# TODO: Create a playlist (that should be allowed only if this is acting as
|
||||
# a ground truth adapter)
|
||||
# cache_adapter.create_playlist()
|
||||
|
||||
adapter.get_playlists()
|
||||
# TODO: verify playlist
|
||||
|
||||
|
||||
def test_caching_get_playlist_details(
|
||||
cache_adapter: FilesystemAdapter,
|
||||
tmp_path: Path,
|
||||
):
|
||||
with pytest.raises(CacheMissError):
|
||||
cache_adapter.get_playlist_details('1')
|
||||
|
||||
# Ingest an empty list (for example, no playlists added yet to server).
|
||||
cache_adapter.ingest_new_data(
|
||||
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS, ('1', ),
|
||||
SubsonicAPI.PlaylistWithSongs('1', 'test1', songs=[]))
|
||||
|
||||
playlist = cache_adapter.get_playlist_details('1')
|
||||
assert playlist.id == '1'
|
||||
assert playlist.name == 'test1'
|
||||
|
||||
with pytest.raises(CacheMissError):
|
||||
cache_adapter.get_playlist_details('2')
|
||||
|
||||
|
||||
def test_no_caching_get_playlist_details(
|
||||
adapter: FilesystemAdapter,
|
||||
tmp_path: Path,
|
||||
):
|
||||
with pytest.raises(Exception):
|
||||
adapter.get_playlist_details('1')
|
||||
|
||||
# TODO: Create a playlist (that should be allowed only if this is acting as
|
||||
# a ground truth adapter)
|
||||
# cache_adapter.create_playlist()
|
||||
|
||||
# adapter.get_playlist_details('1')
|
||||
# TODO: verify playlist details
|
||||
|
Reference in New Issue
Block a user