Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beets API tests #46

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging
import os
import unittest

from mopidy_beets.actor import BeetsBackend


TEST_DATA_DIRECTORY = os.path.join(os.path.dirname(__file__), "data")


class MopidyBeetsTest(unittest.TestCase):
@staticmethod
def get_config():
config = {}
config["enabled"] = True
config["hostname"] = "example.org"
config["port"] = 8337
return {"beets": config, "proxy": {}}

def setUp(self):
super().setUp()
logging.getLogger("mopidy_beets.library").disabled = True
config = self.get_config()
self.backend = BeetsBackend(config=config, audio=None)
9 changes: 9 additions & 0 deletions tests/data/beets-rsrc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Minimal replication of beets' ressource directory

The `BeetsHelper.add_fixture` method relies on a file named `min.mp3` to exist in the
*beets* ressource directory.
By default *beets* assumes its ressource directory to be located right below the `test/`
directory in the *beets* repository.
But this path is not part of the *beets* package.
Thus, we add the minimal amount of necessary files and manipulate the location stored in
`beets.test._common.RSRC`.
Empty file added tests/data/beets-rsrc/min.mp3
Empty file.
132 changes: 132 additions & 0 deletions tests/helper_beets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import collections
import logging
import os
import random
import threading
import time
import typing

import beets.test._common
import werkzeug.serving
from beets.util import bytestring_path
from beets.test.helper import TestHelper as BeetsTestHelper
from beetsplug.web import app as beets_web_app

from . import MopidyBeetsTest, TEST_DATA_DIRECTORY


BeetsTrack = collections.namedtuple(
"BeetsTrack", ("title", "artist", "track"), defaults=(None, None)
)
BeetsAlbum = collections.namedtuple(
"BeetsAlbum",
("title", "artist", "tracks", "genre", "year"),
defaults=("", 0),
)


# Manipulate beets's ressource path before any action wants to access these files.
beets.test._common.RSRC = bytestring_path(
os.path.abspath(os.path.join(TEST_DATA_DIRECTORY, "beets-rsrc"))
)


class BeetsLibrary(BeetsTestHelper):
"""Provide a temporary Beets library for testing against a real Beets web plugin."""

def __init__(
self,
bind_host: str = "127.0.0.1",
bind_port: typing.Optional[int] = None,
) -> None:
self._app = beets_web_app
# allow exceptions to propagate to the caller of the test client
self._app.testing = True
self._bind_host = bind_host
if bind_port is None:
self._bind_port = random.randint(10000, 32767)
else:
self._bind_port = bind_port
self._server = None

self.setup_beets(disk=True)
self._app.config["lib"] = self.lib
self.load_plugins("web")
# prepare the server instance
self._server = werkzeug.serving.make_server(
self._bind_host, self._bind_port, self._app
)
self._server_thread = threading.Thread(
target=self._server.serve_forever
)

def start(self):
self._server_thread.start()
# wait for the server to be ready
while self._server is None:
time.sleep(0.1)

def stop(self):
if self._server_thread is not None:
self._server.shutdown()
self._server_thread.join()
self._server_thread = None

def get_connection_pair(self):
return (self._bind_host, self._bind_port)


class BeetsAPILibraryTest(MopidyBeetsTest):
"""Mixin for MopidyBeetsTest providing access to a temporary Beets library.

Supported features:
- import the albums defined in the 'BEETS_ALBUMS' class variable into the Beets
library
- accesses to `self.backend.library` will query the Beets library via the web plugin
"""

BEETS_ALBUMS: list[BeetsAlbum] = []

def setUp(self):
logging.getLogger("beets").disabled = True
logging.getLogger("werkzeug").disabled = True
self.beets = BeetsLibrary()
# set the host and port of the beets API in our class-based configuration
config = self.get_config()
host, port = self.beets.get_connection_pair()
config["beets"]["hostname"] = host
config["beets"]["port"] = port
self.get_config = lambda: config
# we call our parent initializer late, since we needed to adjust its config
super().setUp()
# Run the thread as late as possible in order to avoid hangs due to exceptions.
# Such exceptions would cause `tearDown` to be skipped.
self.beets.start()
self.beets_populate()

def beets_populate(self) -> None:
"""Import the albums specified in the class variable 'BEETS_ALBUMS'."""
for album in self.BEETS_ALBUMS:
album_items = []
for track_index, track_data in enumerate(album.tracks):
args = {
"album": album.title,
"albumartist": album.artist,
"genre": album.genre,
"artist": track_data.artist,
"title": track_data.title,
"track": track_data.track,
"year": album.year,
}
for key, fallback_value in {
"artist": album.artist,
"track": track_index,
}.items():
if args[key] is None:
args[key] = fallback_value
new_item = self.beets.add_item_fixture(**args)
album_items.append(new_item)
self.beets.lib.add_album(album_items)

def tearDown(self):
self.beets.stop()
63 changes: 63 additions & 0 deletions tests/test_beets_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from .helper_beets import BeetsAlbum, BeetsAPILibraryTest, BeetsTrack


class LookupTest(BeetsAPILibraryTest):
BEETS_ALBUMS = [
BeetsAlbum(
"Album-Title-1",
"Album-Artist-1",
[
BeetsTrack("Title-1"),
BeetsTrack("Title-2"),
BeetsTrack("Title-3"),
],
"Genre-1",
2012,
),
BeetsAlbum(
"Album-Title-2",
"Album-Artist-2",
[BeetsTrack("Title-1")],
),
]

BROWSE_CATEGORIES = (
"albums-by-artist",
"albums-by-genre",
"albums-by-year",
)

def get_uri(self, *components):
return ":".join(("beets", "library") + components)

def test_categories(self):
response = self.backend.library.browse("beets:library")
self.assertEqual(len(response), len(self.BROWSE_CATEGORIES))
for category in self.BROWSE_CATEGORIES:
with self.subTest(category=category):
full_category = self.get_uri(category)
self.assertIn(full_category, (item.uri for item in response))

def test_browse_albums_by_artist(self):
response = self.backend.library.browse("beets:library:albums-by-artist")
expected_album_artists = sorted(
album.artist for album in self.BEETS_ALBUMS
)
received_album_artists = [item.name for item in response]
self.assertEqual(received_album_artists, expected_album_artists)

def test_browse_albums_by_genre(self):
response = self.backend.library.browse("beets:library:albums-by-genre")
expected_album_genres = sorted(
album.genre for album in self.BEETS_ALBUMS
)
received_album_genres = [item.name for item in response]
self.assertEqual(received_album_genres, expected_album_genres)

def test_browse_albums_by_year(self):
response = self.backend.library.browse("beets:library:albums-by-year")
expected_album_genres = sorted(
str(album.year) for album in self.BEETS_ALBUMS
)
received_album_genres = [item.name for item in response]
self.assertEqual(received_album_genres, expected_album_genres)
21 changes: 18 additions & 3 deletions tests/test_extension.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import unittest
from unittest import mock

from mopidy_beets import BeetsExtension
from mopidy_beets.actor import BeetsBackend

from . import MopidyBeetsTest

class ExtensionTest(unittest.TestCase):

class ExtensionTest(MopidyBeetsTest):
def test_get_default_config(self):
ext = BeetsExtension()

Expand All @@ -23,4 +26,16 @@ def test_get_config_schema(self):
self.assertIn("hostname", schema)
self.assertIn("port", schema)

# TODO Write more tests
def test_get_backend_classes(self):
registry = mock.Mock()
ext = BeetsExtension()
ext.setup(registry)
self.assertIn(
mock.call("backend", BeetsBackend), registry.add.mock_calls
)

def test_init_backend(self):
backend = BeetsBackend(self.get_config(), None)
self.assertIsNotNone(backend)
backend.on_start()
backend.on_stop()
7 changes: 7 additions & 0 deletions tests/test_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from . import MopidyBeetsTest


class LibraryTest(MopidyBeetsTest):
def test_invalid_uri(self):
refs = self.backend.library.lookup("beets:invalid_uri")
self.assertEqual(refs, [])
7 changes: 6 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ envlist = py39, py310, py311, check-manifest, flake8

[testenv]
sitepackages = true
deps = .[test]
deps =
.[test]
# we need the `beets.test` module, which is not part of a release, yet (2024-01)
beets@git+https://github.com/beetbox/beets.git@master
flask
werkzeug
commands =
python -m pytest \
--basetemp={envtmpdir} \
Expand Down