Skip to content

Commit

Permalink
tests: add test for browsing the beets library
Browse files Browse the repository at this point in the history
The test use a real beets library and access it via the beets web
plugin.
Thus, the test can verify the full processing chain from importing into
beets through the web API queries up to the assembly of the reference
list.
  • Loading branch information
sumpfralle committed Jan 22, 2024
1 parent 9c36f2e commit 89e5d82
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 1 deletion.
4 changes: 4 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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():
Expand Down
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)
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

0 comments on commit 89e5d82

Please sign in to comment.