mirror of
https://github.com/beetbox/beets.git
synced 2026-07-22 03:17:26 -04:00
Merge branch 'master' into tidal
This commit is contained in:
@@ -37,7 +37,7 @@ from dataclasses import dataclass
|
||||
from functools import cache, cached_property
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir, mkdtemp, mkstemp
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -59,7 +59,10 @@ from beets.util import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from requests_mock.mocker import Mocker
|
||||
from typing_extensions import Self
|
||||
|
||||
RUNNING_IN_CI = os.environ.get("GITHUB_ACTIONS") == "true"
|
||||
|
||||
@@ -149,12 +152,29 @@ class IOMixin(RunMixin):
|
||||
class TestHelper(RunMixin, ConfigMixin):
|
||||
"""Helper mixin for high-level cli and plugin tests.
|
||||
|
||||
This mixin provides methods to isolate beets' global state provide
|
||||
fixtures.
|
||||
This mixin provides methods to isolate beets' global state.
|
||||
|
||||
You may use it as a context manager in pytest fixtures in order to setup
|
||||
tests at a class or module level. See ``module_helper`` and ``class_helper``
|
||||
fixtures, for example.
|
||||
"""
|
||||
|
||||
request: pytest.FixtureRequest
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
self.setup_beets()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> Literal[False]:
|
||||
self.teardown_beets()
|
||||
# return False/None to propagate exceptions
|
||||
return False
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, request: pytest.FixtureRequest):
|
||||
self.request = request
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
from beets.autotag import Distance
|
||||
from beets.dbcore.query import Query
|
||||
from beets.test._common import DummyIO
|
||||
from beets.test.helper import RUNNING_IN_CI, ConfigMixin
|
||||
from beets.test.helper import RUNNING_IN_CI, ConfigMixin, TestHelper
|
||||
from beets.test.helper import is_importable as check_import
|
||||
from beets.util import cached_classproperty
|
||||
|
||||
@@ -129,3 +129,45 @@ def is_importable():
|
||||
"""Fixture that provides a function to check if a module can be imported."""
|
||||
|
||||
return check_import
|
||||
|
||||
|
||||
# Shared fixtures amortize the expensive TestHelper setup across multiple tests.
|
||||
#
|
||||
# TestHelper resets and reloads the beets configuration, so recreating it for
|
||||
# every test can slow large suites noticeably.
|
||||
#
|
||||
# Inheriting from TestHelper gives each test function isolated state. Use the
|
||||
# fixtures below instead when a broader scope is safe and the suite benefits
|
||||
# from reusing the same helper instance.
|
||||
@pytest.fixture(scope="session")
|
||||
def session_helper():
|
||||
"""Share beets test state across the full test session.
|
||||
|
||||
Use this for suites that tolerate shared library contents and global
|
||||
configuration. Tests should target specific records rather than assume a
|
||||
completely fresh overall state.
|
||||
"""
|
||||
with TestHelper() as helper:
|
||||
yield helper
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def module_helper():
|
||||
"""Share beets test state within one test module.
|
||||
|
||||
Use this when tests in the same file can reuse setup and side effects, but
|
||||
later modules should still begin from a clean environment.
|
||||
"""
|
||||
with TestHelper() as helper:
|
||||
yield helper
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def class_helper():
|
||||
"""Share beets test state within one test class.
|
||||
|
||||
Use this when methods in a class can build on the same setup, while nearby
|
||||
classes still need independent libraries, files, or configuration.
|
||||
"""
|
||||
with TestHelper() as helper:
|
||||
yield helper
|
||||
|
||||
@@ -8,14 +8,63 @@ from beets.dbcore import types
|
||||
from beets.library import migrations
|
||||
from beets.library.models import Album, Item
|
||||
from beets.test.helper import TestHelper
|
||||
from beets.util import path_as_posix
|
||||
from beets.util import cached_classproperty, path_as_posix
|
||||
|
||||
Migration = tuple[
|
||||
type[migrations.Migration], tuple[type[Item] | type[Album], ...]
|
||||
]
|
||||
|
||||
|
||||
class TestMultiGenreFieldMigration:
|
||||
@pytest.fixture
|
||||
def helper(self, monkeypatch):
|
||||
class MigrationTestHelper(TestHelper):
|
||||
"""Provide a shared harness for exercising one library migration at a time.
|
||||
|
||||
The helper builds a library that starts in a pre-migration state, then
|
||||
re-enables the migration under test so each subclass can verify the data
|
||||
transformation in isolation.
|
||||
"""
|
||||
|
||||
migration: ClassVar[Migration]
|
||||
|
||||
@classmethod
|
||||
def setup_previous_state(cls, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Shape the library into the legacy state expected by the migration.
|
||||
|
||||
Subclasses override this hook to create older schema or metadata
|
||||
layouts before the test library is initialized.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, monkeypatch):
|
||||
"""Initialize the test library around the migration under test.
|
||||
|
||||
The fixture first prevents automatic migrations so legacy test data can
|
||||
be created safely, then restores the target migration and tears down the
|
||||
temporary library after each test.
|
||||
"""
|
||||
# do not apply migrations upon library initialization
|
||||
monkeypatch.setattr("beets.library.library.Library._migrations", ())
|
||||
self.setup_previous_state(monkeypatch)
|
||||
|
||||
self.setup_beets()
|
||||
|
||||
# and now configure the migrations to be tested
|
||||
monkeypatch.setattr(
|
||||
"beets.library.library.Library._migrations", (self.migration,)
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
class TestMultiGenreFieldMigration(MigrationTestHelper):
|
||||
"""Verify legacy single-value genres are promoted to multi-value fields."""
|
||||
|
||||
migration = (migrations.MultiGenreFieldMigration, (Item, Album))
|
||||
|
||||
@classmethod
|
||||
def setup_previous_state(cls, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Expose the legacy genre columns required to exercise the migration."""
|
||||
# add genre field to both models to make sure this column is created
|
||||
monkeypatch.setattr(
|
||||
"beets.library.models.Item._fields",
|
||||
@@ -28,20 +77,10 @@ class TestMultiGenreFieldMigration:
|
||||
monkeypatch.setattr(
|
||||
"beets.library.models.Album.item_keys", {*Album.item_keys, "genre"}
|
||||
)
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
|
||||
# and now configure the migrations to be tested
|
||||
monkeypatch.setattr(
|
||||
"beets.library.library.Library._migrations",
|
||||
((migrations.MultiGenreFieldMigration, (Item, Album)),),
|
||||
)
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
def test_migrate(self, helper: TestHelper):
|
||||
helper.config["lastgenre"]["separator"] = " - "
|
||||
def test_migrate(self):
|
||||
"""Ensure genre data is split once and preserved when already populated."""
|
||||
self.config["lastgenre"]["separator"] = " - "
|
||||
|
||||
expected_item_genres = []
|
||||
for genre, initial_genres, expected_genres in [
|
||||
@@ -54,55 +93,51 @@ class TestMultiGenreFieldMigration:
|
||||
# multiple genres are split the first (lastgenre) separator ONLY
|
||||
("Item - Rock, Alternative", (), ("Item", "Rock, Alternative")),
|
||||
]:
|
||||
helper.add_item(genre=genre, genres=initial_genres)
|
||||
self.add_item(genre=genre, genres=initial_genres)
|
||||
expected_item_genres.append(expected_genres)
|
||||
|
||||
unmigrated_album = helper.add_album(
|
||||
unmigrated_album = self.add_album(
|
||||
genre="Album Rock / Alternative", genres=[]
|
||||
)
|
||||
expected_item_genres.append(("Album Rock", "Alternative"))
|
||||
|
||||
helper.lib._migrate()
|
||||
self.lib._migrate()
|
||||
|
||||
actual_item_genres = [tuple(i.genres) for i in helper.lib.items()]
|
||||
actual_item_genres = [tuple(i.genres) for i in self.lib.items()]
|
||||
assert actual_item_genres == expected_item_genres
|
||||
|
||||
unmigrated_album.load()
|
||||
assert unmigrated_album.genres == ["Album Rock", "Alternative"]
|
||||
|
||||
# remove cached initial db tables data
|
||||
del helper.lib.db_tables
|
||||
assert helper.lib.migration_exists("multi_genre_field", "items")
|
||||
assert helper.lib.migration_exists("multi_genre_field", "albums")
|
||||
del self.lib.db_tables
|
||||
assert self.lib.migration_exists("multi_genre_field", "items")
|
||||
assert self.lib.migration_exists("multi_genre_field", "albums")
|
||||
|
||||
|
||||
class MultiArtistFieldMigrationTestMixin:
|
||||
class MultiArtistFieldMigrationTestMixin(MigrationTestHelper):
|
||||
"""Share coverage for migrations that split artist-like text into lists."""
|
||||
|
||||
str_field: ClassVar[str]
|
||||
list_field: ClassVar[str]
|
||||
migration_cls: ClassVar[type[migrations.MultiValueFieldMigration]]
|
||||
|
||||
@pytest.fixture
|
||||
def helper(self, monkeypatch):
|
||||
# do not apply migrations upon library initialization
|
||||
monkeypatch.setattr("beets.library.library.Library._migrations", ())
|
||||
@cached_classproperty
|
||||
def migration(cls) -> Migration:
|
||||
"""Bind each concrete test class to its corresponding migration."""
|
||||
return (cls.migration_cls, (Item,))
|
||||
|
||||
@classmethod
|
||||
def setup_previous_state(cls, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Expose the legacy scalar field expected by the migration."""
|
||||
# add legacy str field to make sure this column is created
|
||||
monkeypatch.setattr(
|
||||
"beets.library.models.Item._fields",
|
||||
{**Item._fields, self.str_field: types.STRING},
|
||||
{**Item._fields, cls.str_field: types.STRING},
|
||||
)
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
|
||||
# and now configure the migrations to be tested
|
||||
monkeypatch.setattr(
|
||||
"beets.library.library.Library._migrations",
|
||||
((self.migration_cls, (Item,)),),
|
||||
)
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
def test_migrate(self, helper: TestHelper):
|
||||
def test_migrate(self):
|
||||
"""Ensure existing list values win and legacy text splits consistently."""
|
||||
expected_list_values = []
|
||||
for str_value, initial_list_value, expected_list_value in [
|
||||
# already existing value is not overwritten
|
||||
@@ -118,19 +153,19 @@ class MultiArtistFieldMigrationTestMixin:
|
||||
self.str_field: str_value,
|
||||
self.list_field: initial_list_value,
|
||||
}
|
||||
helper.add_item(**data)
|
||||
self.add_item(**data)
|
||||
expected_list_values.append(expected_list_value)
|
||||
|
||||
helper.lib._migrate()
|
||||
self.lib._migrate()
|
||||
|
||||
actual_list_values = [
|
||||
tuple(i[self.list_field]) for i in helper.lib.items()
|
||||
tuple(i[self.list_field]) for i in self.lib.items()
|
||||
]
|
||||
assert actual_list_values == expected_list_values
|
||||
|
||||
# remove cached initial db tables data
|
||||
del helper.lib.db_tables
|
||||
assert helper.lib.migration_exists(
|
||||
del self.lib.db_tables
|
||||
assert self.lib.migration_exists(
|
||||
f"multi_{self.str_field}_field", "items"
|
||||
)
|
||||
|
||||
@@ -159,26 +194,14 @@ class TestMultiArrangerFieldMigration(MultiArtistFieldMigrationTestMixin):
|
||||
migration_cls = migrations.MultiArrangerFieldMigration
|
||||
|
||||
|
||||
class TestLyricsMetadataInFlexFieldsMigration:
|
||||
@pytest.fixture
|
||||
def helper(self, monkeypatch):
|
||||
# do not apply migrations upon library initialization
|
||||
monkeypatch.setattr("beets.library.library.Library._migrations", ())
|
||||
class TestLyricsMetadataInFlexFieldsMigration(MigrationTestHelper):
|
||||
"""Verify embedded lyrics metadata moves into dedicated flexible fields."""
|
||||
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
migration = (migrations.LyricsMetadataInFlexFieldsMigration, (Item,))
|
||||
|
||||
# and now configure the migrations to be tested
|
||||
monkeypatch.setattr(
|
||||
"beets.library.library.Library._migrations",
|
||||
((migrations.LyricsMetadataInFlexFieldsMigration, (Item,)),),
|
||||
)
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
def test_migrate(self, helper: TestHelper, is_importable):
|
||||
lyrics_item = helper.add_item(
|
||||
def test_migrate(self, is_importable):
|
||||
"""Ensure extracted metadata is preserved without mutating plain lyrics."""
|
||||
lyrics_item = self.add_item(
|
||||
lyrics=textwrap.dedent("""
|
||||
[00:00.00] Some synced lyrics / Quelques paroles synchronisées
|
||||
[00:00.50]
|
||||
@@ -186,9 +209,9 @@ class TestLyricsMetadataInFlexFieldsMigration:
|
||||
|
||||
Source: https://lrclib.net/api/1/""")
|
||||
)
|
||||
instrumental_lyrics_item = helper.add_item(lyrics="[Instrumental]")
|
||||
instrumental_lyrics_item = self.add_item(lyrics="[Instrumental]")
|
||||
|
||||
helper.lib._migrate()
|
||||
self.lib._migrate()
|
||||
|
||||
lyrics_item.load()
|
||||
|
||||
@@ -220,37 +243,25 @@ class TestLyricsMetadataInFlexFieldsMigration:
|
||||
instrumental_lyrics_item.lyrics_translation_language
|
||||
|
||||
# remove cached initial db tables data
|
||||
del helper.lib.db_tables
|
||||
assert helper.lib.migration_exists(
|
||||
del self.lib.db_tables
|
||||
assert self.lib.migration_exists(
|
||||
"lyrics_metadata_in_flex_fields", "items"
|
||||
)
|
||||
|
||||
|
||||
class TestRelativePathMigration:
|
||||
@pytest.fixture
|
||||
def helper(self, monkeypatch):
|
||||
# do not apply migrations upon library initialization
|
||||
monkeypatch.setattr("beets.library.library.Library._migrations", ())
|
||||
class TestRelativePathMigration(MigrationTestHelper):
|
||||
"""Verify stored item paths are rewritten to library-relative values."""
|
||||
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
migration = (migrations.RelativePathMigration, (Item,))
|
||||
|
||||
# and now configure the migrations to be tested
|
||||
monkeypatch.setattr(
|
||||
"beets.library.library.Library._migrations",
|
||||
((migrations.RelativePathMigration, (Item,)),),
|
||||
)
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
def test_migrate(self, helper: TestHelper):
|
||||
def test_migrate(self):
|
||||
"""Ensure stored paths become relative while the public API stays stable."""
|
||||
relative_path = os.path.join("foo", "bar", "baz.mp3")
|
||||
abs_string_path = str(helper.lib_path / relative_path)
|
||||
abs_string_path = str(self.lib_path / relative_path)
|
||||
abs_bytes_path = os.fsencode(abs_string_path)
|
||||
|
||||
# need to insert the path directly into the database to bypass the path setter
|
||||
helper.lib._connection().executemany(
|
||||
self.lib._connection().executemany(
|
||||
"INSERT INTO items (id, path) VALUES (?, ?)",
|
||||
(
|
||||
(1, abs_bytes_path),
|
||||
@@ -259,20 +270,20 @@ class TestRelativePathMigration:
|
||||
),
|
||||
)
|
||||
old_stored_path = (
|
||||
helper.lib._connection()
|
||||
self.lib._connection()
|
||||
.execute("select path from items where id=?", (1,))
|
||||
.fetchone()[0]
|
||||
)
|
||||
assert old_stored_path == abs_bytes_path
|
||||
|
||||
helper.lib._migrate()
|
||||
self.lib._migrate()
|
||||
|
||||
item = helper.lib.get_item(1)
|
||||
item = self.lib.get_item(1)
|
||||
assert item
|
||||
|
||||
# and now we have a relative path
|
||||
stored_path = (
|
||||
helper.lib._connection()
|
||||
self.lib._connection()
|
||||
.execute("select path from items where id=?", (item.id,))
|
||||
.fetchone()[0]
|
||||
)
|
||||
@@ -281,6 +292,6 @@ class TestRelativePathMigration:
|
||||
assert item.path == abs_bytes_path
|
||||
|
||||
# also check the string path was migrated correctly
|
||||
str_item = helper.lib.get_item(2)
|
||||
str_item = self.lib.get_item(2)
|
||||
assert str_item
|
||||
assert str_item.path == abs_bytes_path
|
||||
|
||||
@@ -19,8 +19,6 @@ import pytest
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beets.ui import UserError
|
||||
|
||||
PLUGIN_NAME = "advancedrewrite"
|
||||
|
||||
|
||||
class AdvancedRewritePluginTest(PluginTestCase):
|
||||
plugin = "advancedrewrite"
|
||||
|
||||
@@ -7,21 +7,23 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
|
||||
from beets.test.helper import TestHelper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from flask.testing import Client
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def helper():
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
yield helper
|
||||
helper.teardown_beets()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def helper(session_helper):
|
||||
"""Keep the helper temp dir alive past the module-scoped Flask app.
|
||||
|
||||
``create_app`` opens a configured SQLite library before tests replace it
|
||||
with ``helper.lib``. On Windows, module teardown can otherwise try to
|
||||
delete ``library.db`` while the app still holds a file handle. The API
|
||||
assertions filter known data, so sharing state for the session is harmless.
|
||||
"""
|
||||
return session_helper
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def app(helper):
|
||||
from beetsplug.aura import create_app
|
||||
|
||||
@@ -30,7 +32,7 @@ def app(helper):
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope="module")
|
||||
def item(helper):
|
||||
return helper.add_item_fixture(
|
||||
album="Album",
|
||||
@@ -40,12 +42,12 @@ def item(helper):
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope="module")
|
||||
def album(helper, item):
|
||||
return helper.lib.add_album([item])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _other_album_and_item(helper):
|
||||
"""Add another item and album to prove that filtering works."""
|
||||
item = helper.add_item_fixture(
|
||||
|
||||
@@ -124,7 +124,6 @@ class EditCommandTest(IOMixin, EditMixin, BeetsTestCase):
|
||||
simulated using `ModifyFileMocker`.
|
||||
"""
|
||||
|
||||
ALBUM_COUNT = 1
|
||||
TRACK_COUNT = 10
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -33,7 +33,7 @@ from beets.test.helper import (
|
||||
FetchImageHelper,
|
||||
ImportHelper,
|
||||
IOMixin,
|
||||
PluginTestHelper,
|
||||
PluginMixin,
|
||||
)
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
@@ -78,18 +78,7 @@ def require_artresizer_compare(test):
|
||||
return wrapper
|
||||
|
||||
|
||||
class PytestImportHelper(PluginTestHelper, ImportHelper):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_import_helper(self, setup):
|
||||
self.import_media = []
|
||||
self.lib.path_formats = [
|
||||
("default", os.path.join("$artist", "$album", "$title")),
|
||||
("singleton:true", os.path.join("singletons", "$title")),
|
||||
("comp:true", os.path.join("compilations", "$album", "$title")),
|
||||
]
|
||||
|
||||
|
||||
class TestEmbedartCli(PytestImportHelper, IOMixin, FetchImageHelper):
|
||||
class TestEmbedartCli(PluginMixin, IOMixin, ImportHelper, FetchImageHelper):
|
||||
plugin = "embedart"
|
||||
small_artpath = os.path.join(_common.RSRC, b"image-2x3.jpg")
|
||||
abbey_artpath = os.path.join(_common.RSRC, b"abbey.jpg")
|
||||
|
||||
@@ -15,24 +15,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from beets.test.helper import PluginMixin, TestHelper
|
||||
from beets.test.helper import PluginTestHelper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def helper(request):
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
|
||||
request.instance.lib = helper.lib
|
||||
request.instance.add_item = helper.add_item
|
||||
|
||||
yield
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("helper")
|
||||
class TestFuzzyPlugin(PluginMixin):
|
||||
class TestFuzzyPlugin(PluginTestHelper):
|
||||
plugin = "fuzzy"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -27,7 +27,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
from beets.library import Item
|
||||
from beets.test.helper import PluginMixin, TestHelper
|
||||
from beets.test.helper import PluginMixin
|
||||
from beets.util.lyrics import Lyrics
|
||||
from beetsplug import lyrics
|
||||
|
||||
@@ -46,11 +46,13 @@ PHRASE_BY_TITLE = {
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def helper():
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
yield helper
|
||||
helper.teardown_beets()
|
||||
def helper(module_helper):
|
||||
"""Reuse one module helper for explicit item and media-write checks.
|
||||
|
||||
Helper-backed tests mutate known items and write an MP3 fixture, but they do
|
||||
not assert against the full library shared with other tests in this module.
|
||||
"""
|
||||
return module_helper
|
||||
|
||||
|
||||
class TestLyricsUtils:
|
||||
|
||||
@@ -8,27 +8,16 @@ import pytest
|
||||
|
||||
from beets.autotag import AlbumInfo, TrackInfo
|
||||
from beets.library import Album, Item
|
||||
from beets.test.helper import IOMixin, PluginMixin, TestHelper
|
||||
from beets.test.helper import IOMixin, PluginTestHelper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def helper(request):
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
|
||||
request.instance.lib = helper.lib
|
||||
|
||||
yield
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("helper")
|
||||
class TestMissingAlbums(IOMixin, PluginMixin):
|
||||
"""Tests for missing albums functionality."""
|
||||
|
||||
class MissingTestHelper(IOMixin, PluginTestHelper):
|
||||
plugin = "missing"
|
||||
|
||||
|
||||
class TestMissingAlbums(MissingTestHelper):
|
||||
"""Tests for missing albums functionality."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"release_from_mb,expected_output",
|
||||
[
|
||||
@@ -176,12 +165,9 @@ class TestMissingAlbums(IOMixin, PluginMixin):
|
||||
assert output == "1\n"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("helper")
|
||||
class TestMissingTracks(IOMixin, PluginMixin):
|
||||
class TestMissingTracks(MissingTestHelper):
|
||||
"""Tests for missing tracks functionality."""
|
||||
|
||||
plugin = "missing"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"total,count,expected",
|
||||
[(True, False, "1\n"), (False, True, "artist - album: 1")],
|
||||
|
||||
@@ -51,10 +51,6 @@ def label_info_factory(**kwargs) -> mb.LabelInfo:
|
||||
return factories.LabelInfoFactory.build(**kwargs)
|
||||
|
||||
|
||||
def release_group_factory(**kwargs) -> mb.ReleaseGroup:
|
||||
return factories.ReleaseGroupFactory.build(**kwargs)
|
||||
|
||||
|
||||
def recording_factory(**kwargs) -> mb.Recording:
|
||||
return factories.RecordingFactory.build(**kwargs)
|
||||
|
||||
|
||||
@@ -19,18 +19,17 @@ import random
|
||||
|
||||
import pytest
|
||||
|
||||
from beets.test.helper import TestHelper
|
||||
from beetsplug.random import _equal_chance_permutation, random_objs
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def helper():
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
def helper(class_helper):
|
||||
"""Use class scope because each random test class owns its item list.
|
||||
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
The helper creates lightweight item objects for class setup; tests do not
|
||||
need a fresh beets temp dir per method or shared state between classes.
|
||||
"""
|
||||
return class_helper
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for the 'subsonic' plugin."""
|
||||
|
||||
import unittest
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import responses
|
||||
|
||||
@@ -9,21 +8,6 @@ from beets import config
|
||||
from beetsplug import subsonicupdate
|
||||
|
||||
|
||||
class ArgumentsMock:
|
||||
"""Argument mocks for tests."""
|
||||
|
||||
def __init__(self, mode, show_failures):
|
||||
"""Constructs ArgumentsMock."""
|
||||
self.mode = mode
|
||||
self.show_failures = show_failures
|
||||
self.verbose = 1
|
||||
|
||||
|
||||
def _params(url):
|
||||
"""Get the query parameters from a URL."""
|
||||
return parse_qs(urlparse(url).query)
|
||||
|
||||
|
||||
class SubsonicPluginTest(unittest.TestCase):
|
||||
"""Test class for subsonicupdate."""
|
||||
|
||||
|
||||
@@ -22,38 +22,6 @@ from beets.library import Item
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beetsplug.titlecase import TitlecasePlugin
|
||||
|
||||
titlecase_fields_testcases = [
|
||||
(
|
||||
{
|
||||
"fields": [
|
||||
"artist",
|
||||
"albumartist",
|
||||
"title",
|
||||
"album",
|
||||
"mb_albumd",
|
||||
"year",
|
||||
],
|
||||
"force_lowercase": True,
|
||||
},
|
||||
Item(
|
||||
artist="OPHIDIAN",
|
||||
albumartist="ophiDIAN",
|
||||
format="CD",
|
||||
year=2003,
|
||||
album="BLACKBOX",
|
||||
title="KhAmElEoN",
|
||||
),
|
||||
Item(
|
||||
artist="Ophidian",
|
||||
albumartist="Ophidian",
|
||||
format="CD",
|
||||
year=2003,
|
||||
album="Blackbox",
|
||||
title="Khameleon",
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class TestTitlecasePlugin(PluginTestCase):
|
||||
plugin = "titlecase"
|
||||
|
||||
@@ -12,22 +12,13 @@ from beetsplug._utils.playcount import (
|
||||
LOGGER_NAME = "beets.test_playcount"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def helper(request):
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
class TestPlayCount(TestHelper):
|
||||
@pytest.fixture
|
||||
def log(self):
|
||||
log = logging.getLogger(LOGGER_NAME)
|
||||
log.set_global_level(logging.DEBUG)
|
||||
return log
|
||||
|
||||
request.instance.lib = helper.lib
|
||||
request.instance.log = logging.getLogger(LOGGER_NAME)
|
||||
request.instance.log.set_global_level(logging.DEBUG)
|
||||
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("helper")
|
||||
class TestPlayCount:
|
||||
def track(self, **overrides):
|
||||
return {
|
||||
"mbid": "",
|
||||
@@ -96,19 +87,17 @@ class TestPlayCount:
|
||||
],
|
||||
)
|
||||
def test_get_items_matches_supported_query_paths(
|
||||
self, item_kwargs, track_kwargs
|
||||
self, log, item_kwargs, track_kwargs
|
||||
):
|
||||
item = self.add_item(**item_kwargs)
|
||||
matched_ids = [
|
||||
matched.id
|
||||
for matched in get_items(
|
||||
self.lib, self.track(**track_kwargs), self.log
|
||||
)
|
||||
for matched in get_items(self.lib, self.track(**track_kwargs), log)
|
||||
]
|
||||
|
||||
assert matched_ids == [item.id]
|
||||
|
||||
def test_process_track_updates_every_matching_song(self):
|
||||
def test_process_track_updates_every_matching_song(self, log):
|
||||
first = self.add_item(
|
||||
title="Song", artist="Artist", album="First Album", play_count=1
|
||||
)
|
||||
@@ -117,14 +106,14 @@ class TestPlayCount:
|
||||
)
|
||||
|
||||
assert (
|
||||
process_track(self.lib, self.track(playcount=0), self.log, "lastfm")
|
||||
process_track(self.lib, self.track(playcount=0), log, "lastfm")
|
||||
is True
|
||||
)
|
||||
|
||||
assert self.get_playcount(first.id) == 0
|
||||
assert self.get_playcount(second.id) == 0
|
||||
|
||||
def test_process_track_returns_false_when_nothing_matches(self):
|
||||
def test_process_track_returns_false_when_nothing_matches(self, log):
|
||||
assert (
|
||||
process_track(
|
||||
self.lib,
|
||||
@@ -134,22 +123,19 @@ class TestPlayCount:
|
||||
album="Missing Album",
|
||||
playcount=4,
|
||||
),
|
||||
self.log,
|
||||
log,
|
||||
"lastfm",
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
def test_process_track_updates_requested_source_field(self):
|
||||
def test_process_track_updates_requested_source_field(self, log):
|
||||
new_count = 6
|
||||
item = self.add_item(play_count=1, source="lastfm")
|
||||
|
||||
assert (
|
||||
process_track(
|
||||
self.lib,
|
||||
self.track(playcount=new_count),
|
||||
self.log,
|
||||
"listenbrainz",
|
||||
self.lib, self.track(playcount=new_count), log, "listenbrainz"
|
||||
)
|
||||
is True
|
||||
)
|
||||
@@ -179,6 +165,7 @@ class TestPlayCount:
|
||||
)
|
||||
def test_update_play_counts_counts_and_logs_summary(
|
||||
self,
|
||||
log,
|
||||
tracks,
|
||||
expected_counts,
|
||||
expected_summary,
|
||||
@@ -192,7 +179,7 @@ class TestPlayCount:
|
||||
update_play_counts(
|
||||
self.lib,
|
||||
[self.track(**track) for track in tracks],
|
||||
self.log,
|
||||
log,
|
||||
"lastfm",
|
||||
)
|
||||
== expected_counts
|
||||
|
||||
@@ -40,20 +40,12 @@ class DummyIMBackend(IMBackend):
|
||||
self.compare_cmd = ["magick", "compare"]
|
||||
|
||||
|
||||
class DummyPILBackend(PILBackend):
|
||||
"""An `PILBackend` which pretends that PIL is available."""
|
||||
|
||||
def __init__(self):
|
||||
"""Init a dummy backend class for mocked PIL tests."""
|
||||
|
||||
|
||||
class ArtResizerFileSizeTest(CleanupModulesMixin, BeetsTestCase):
|
||||
"""Unittest test case for Art Resizer to a specific filesize."""
|
||||
|
||||
modules = (IMBackend.__module__,)
|
||||
|
||||
IMG_225x225 = os.path.join(_common.RSRC, b"abbey.jpg")
|
||||
IMG_225x225_SIZE = os.stat(syspath(IMG_225x225)).st_size
|
||||
|
||||
def _test_img_resize(self, backend):
|
||||
"""Test resizing based on file size, given a resize_func."""
|
||||
|
||||
@@ -147,10 +147,6 @@ class ModelFixture5(ModelFixture1):
|
||||
}
|
||||
|
||||
|
||||
class DatabaseFixture5(dbcore.Database):
|
||||
_models = (ModelFixture5,)
|
||||
|
||||
|
||||
class DatabaseFixtureTwoModels(dbcore.Database):
|
||||
_models = (ModelFixture2, AnotherModelFixture)
|
||||
|
||||
|
||||
@@ -98,21 +98,13 @@ class TestPluginRegistration(IOMixin, PluginTestHelper):
|
||||
assert out == "one; two; three\n"
|
||||
|
||||
|
||||
class PytestImportHelper(ImportHelper, PluginTestHelper):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_import_helper(self, setup):
|
||||
self.import_media = []
|
||||
self.lib.path_formats = [
|
||||
("default", os.path.join("$artist", "$album", "$title")),
|
||||
("singleton:true", os.path.join("singletons", "$title")),
|
||||
("comp:true", os.path.join("compilations", "$album", "$title")),
|
||||
]
|
||||
|
||||
#
|
||||
class PluginImportHelper(PluginMixin, ImportHelper):
|
||||
def setup_beets(self):
|
||||
super().setup_beets()
|
||||
self.prepare_album_for_import(2)
|
||||
|
||||
|
||||
class TestEvents(PytestImportHelper):
|
||||
class TestEvents(PluginImportHelper):
|
||||
def test_import_task_created(self, caplog):
|
||||
self.importer = self.setup_importer(pretend=True)
|
||||
|
||||
@@ -282,7 +274,7 @@ class TestListeners(PluginTestHelper):
|
||||
plugins.send("event9", foo=5)
|
||||
|
||||
|
||||
class TestPromptChoices(TerminalImportMixin, PytestImportHelper):
|
||||
class TestPromptChoices(TerminalImportMixin, PluginImportHelper):
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_prompt_choice(self, io):
|
||||
self.setup_importer()
|
||||
|
||||
@@ -44,7 +44,6 @@ from beets.dbcore.query import (
|
||||
)
|
||||
from beets.library import Item
|
||||
from beets.test import _common
|
||||
from beets.test.helper import TestHelper
|
||||
|
||||
# Because the absolute path begins with something like C:, we
|
||||
# can't disambiguate it from an ordinary query.
|
||||
@@ -54,13 +53,13 @@ _p = pytest.param
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def helper():
|
||||
helper = TestHelper()
|
||||
helper.setup_beets()
|
||||
def helper(class_helper):
|
||||
"""Use class scope because each query class owns a library shape.
|
||||
|
||||
yield helper
|
||||
|
||||
helper.teardown_beets()
|
||||
Class fixtures add rows, albums, flex types, paths, or media files and then
|
||||
query whole collections, so sibling class data would change expectations.
|
||||
"""
|
||||
return class_helper
|
||||
|
||||
|
||||
class TestGet:
|
||||
|
||||
Reference in New Issue
Block a user