Added tests for the filesystem adapter

This commit is contained in:
Sumner Evans
2020-04-22 00:27:05 -06:00
parent 2863570e5b
commit c90b52f493
6 changed files with 175 additions and 38 deletions

View File

@@ -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,
):

View File

@@ -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(),
)

View File

@@ -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()

View File

@@ -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,
)

View File

@@ -3,7 +3,7 @@ import pytest
from sublime.adapters import Adapter, AdapterManager
class TestAdapter(Adapter):
class MyAdapter(Adapter):
pass

View File

@@ -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