diff --git a/beets/test/helper.py b/beets/test/helper.py index 0acebfaf6..51fee2b36 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -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 diff --git a/test/conftest.py b/test/conftest.py index 75a40bee4..8d47b5fdc 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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 diff --git a/test/library/test_migrations.py b/test/library/test_migrations.py index d2b3f5429..a988c5f62 100644 --- a/test/library/test_migrations.py +++ b/test/library/test_migrations.py @@ -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 diff --git a/test/plugins/test_advancedrewrite.py b/test/plugins/test_advancedrewrite.py index a00afbfc6..e5ecb7b73 100644 --- a/test/plugins/test_advancedrewrite.py +++ b/test/plugins/test_advancedrewrite.py @@ -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" diff --git a/test/plugins/test_aura.py b/test/plugins/test_aura.py index 188c44c9e..92dc4c883 100644 --- a/test/plugins/test_aura.py +++ b/test/plugins/test_aura.py @@ -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( diff --git a/test/plugins/test_edit.py b/test/plugins/test_edit.py index 432c9ba8c..1670c776c 100644 --- a/test/plugins/test_edit.py +++ b/test/plugins/test_edit.py @@ -124,7 +124,6 @@ class EditCommandTest(IOMixin, EditMixin, BeetsTestCase): simulated using `ModifyFileMocker`. """ - ALBUM_COUNT = 1 TRACK_COUNT = 10 def setUp(self): diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index b197f338c..25e9e04fa 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -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") diff --git a/test/plugins/test_fuzzy.py b/test/plugins/test_fuzzy.py index be55b76ee..dc1b093bc 100644 --- a/test/plugins/test_fuzzy.py +++ b/test/plugins/test_fuzzy.py @@ -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( diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py index 285a8ad7f..220d57a45 100644 --- a/test/plugins/test_lyrics.py +++ b/test/plugins/test_lyrics.py @@ -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: diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index de74ac6c4..b04013944 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -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")], diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index a31e2854e..1aded1db8 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -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) diff --git a/test/plugins/test_random.py b/test/plugins/test_random.py index 975bb8ffa..9df6cf022 100644 --- a/test/plugins/test_random.py +++ b/test/plugins/test_random.py @@ -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) diff --git a/test/plugins/test_subsonicupdate.py b/test/plugins/test_subsonicupdate.py index 183c2bd67..1eb2dee98 100644 --- a/test/plugins/test_subsonicupdate.py +++ b/test/plugins/test_subsonicupdate.py @@ -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.""" diff --git a/test/plugins/test_titlecase.py b/test/plugins/test_titlecase.py index f392cd69f..c098550a8 100644 --- a/test/plugins/test_titlecase.py +++ b/test/plugins/test_titlecase.py @@ -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" diff --git a/test/plugins/utils/test_playcount.py b/test/plugins/utils/test_playcount.py index f0ffec588..e77e2b427 100644 --- a/test/plugins/utils/test_playcount.py +++ b/test/plugins/utils/test_playcount.py @@ -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 diff --git a/test/test_art_resize.py b/test/test_art_resize.py index 9b52d95e9..56add6af8 100644 --- a/test/test_art_resize.py +++ b/test/test_art_resize.py @@ -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.""" diff --git a/test/test_dbcore.py b/test/test_dbcore.py index d84a25553..283a3c6c2 100644 --- a/test/test_dbcore.py +++ b/test/test_dbcore.py @@ -147,10 +147,6 @@ class ModelFixture5(ModelFixture1): } -class DatabaseFixture5(dbcore.Database): - _models = (ModelFixture5,) - - class DatabaseFixtureTwoModels(dbcore.Database): _models = (ModelFixture2, AnotherModelFixture) diff --git a/test/test_plugins.py b/test/test_plugins.py index 9437ba51a..6ccef86cd 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -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() diff --git a/test/test_query.py b/test/test_query.py index 05b37656b..f6fb7b7d6 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -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: