Files
beets/test/plugins/test_chroma.py
Andrew Yang 4f3e8907c4 test(chroma): consolidate gating tests into a single class
Per @snejus review feedback on #6532: merge TestChromaWithoutMusicBrainz
and TestChromaWithMusicBrainz into a single TestChromaCandidates class
with the motivating comment moved into its docstring. Test method names
now encode the 'without' / 'with' variant.
2026-04-15 00:03:23 +01:00

192 lines
6.3 KiB
Python

# This file is part of beets.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from unittest.mock import MagicMock, patch
import pytest
from beets import metadata_plugins
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Item
from beets.test.helper import ImportTestCase, IOMixin, PluginMixin
from beetsplug import chroma
TEST_TITLE_1 = "TEST_TITLE_1"
TEST_TITLE_2 = "TEST_TITLE_2"
FINGERPRINT_1 = "FP_1"
FINGERPRINT_1_CLOSE = "FP_1_CLOSE"
FINGERPRINT_2 = "FP_2"
@patch("acoustid.compare_fingerprints")
class ChromaTest(IOMixin, PluginMixin, ImportTestCase):
plugin = "chroma"
def setup_lib(self):
item1 = Item(path="/file")
item1.length = 30
item1.title = TEST_TITLE_1
item1.acoustid_fingerprint = FINGERPRINT_1
item1.add(self.lib)
item2 = Item(path="/file")
item2.length = 30
item2.title = TEST_TITLE_2
item2.acoustid_fingerprint = FINGERPRINT_2
item2.add(self.lib)
def run_search(self, fp):
return self.run_with_output("chromasearch", "-s", fp, "-f", "$title")
def line_count(self, str):
return len(
[line for line in str.split("\n") if line.strip(" \n") != ""]
)
def compare_fingerprints(self, *args, **kwargs):
if args[0][1] == args[1][1]:
return 1
if args[0][1] == FINGERPRINT_1_CLOSE and args[1][1] == FINGERPRINT_1:
return 0.9
return 0.1
def test_chroma_search_exact(self, compare_fingerprints):
self.setup_lib()
compare_fingerprints.side_effect = self.compare_fingerprints
output = self.run_search(FINGERPRINT_2)
assert self.line_count(output) == 1
assert TEST_TITLE_2 in output
output = self.run_search(FINGERPRINT_1)
assert self.line_count(output) == 1
assert TEST_TITLE_1 in output
def test_chroma_search_close(self, compare_fingerprints):
self.setup_lib()
compare_fingerprints.side_effect = self.compare_fingerprints
output = self.run_search(FINGERPRINT_1_CLOSE)
assert self.line_count(output) == 2
assert TEST_TITLE_1 in output.split("\n")[0]
def _seed_acoustid_match(
item_path: bytes = b"/fake/path.mp3",
recording_ids: list[str] | None = None,
release_ids: list[str] | None = None,
) -> Item:
"""Seed the chroma module-level match cache as if acoustid had run."""
if recording_ids is None:
recording_ids = ["rec-id-1"]
if release_ids is None:
release_ids = ["rel-id-1", "rel-id-1", "rel-id-1"]
chroma._matches[item_path] = (recording_ids, release_ids)
return Item(path=item_path)
class TestChromaCandidates(PluginMixin):
"""Regression tests for issue #6212: chroma must respect which metadata
source plugins are enabled.
When the musicbrainz plugin is not loaded, chroma must not produce any
MusicBrainz-sourced candidates (via either ``candidates`` or
``item_candidates``). When it IS loaded, chroma resolves acoustid
matches through the registered plugin instance.
``plugin`` is intentionally not set on the class so that
:py:meth:`PluginMixin.load_plugins` honours explicit plugin-name
arguments and each test can choose its own combination. The autouse
fixture clears the ``@cache``-decorated metadata-source registry and
the chroma match state between tests.
"""
preload_plugin = False
@pytest.fixture(autouse=True)
def _setup_chroma(self):
metadata_plugins.find_metadata_source_plugins.cache_clear()
metadata_plugins.get_metadata_source.cache_clear()
chroma._matches.clear()
yield
chroma._matches.clear()
self.unload_plugins()
metadata_plugins.find_metadata_source_plugins.cache_clear()
metadata_plugins.get_metadata_source.cache_clear()
def test_candidates_returns_empty_without_musicbrainz(self):
self.load_plugins("chroma")
plugin = chroma.AcoustidPlugin()
item = _seed_acoustid_match()
result = plugin.candidates(
[item], artist="A", album="B", va_likely=False
)
assert list(result) == []
def test_item_candidates_returns_empty_without_musicbrainz(self):
self.load_plugins("chroma")
plugin = chroma.AcoustidPlugin()
item = _seed_acoustid_match()
result = plugin.item_candidates(item, artist="A", title="B")
assert list(result) == []
def test_candidates_returns_mb_albums_with_musicbrainz(self, monkeypatch):
self.load_plugins("chroma", "musicbrainz")
fake_album = AlbumInfo(
tracks=[], album_id="rel-id-1", album="Fake Album"
)
mb_plugin = metadata_plugins.get_metadata_source("musicbrainz")
assert mb_plugin is not None
monkeypatch.setattr(
mb_plugin, "album_for_id", MagicMock(return_value=fake_album)
)
plugin = chroma.AcoustidPlugin()
item = _seed_acoustid_match()
result = list(
plugin.candidates([item], artist="A", album="B", va_likely=False)
)
assert result == [fake_album]
mb_plugin.album_for_id.assert_called_with("rel-id-1")
def test_item_candidates_returns_mb_tracks_with_musicbrainz(
self, monkeypatch
):
self.load_plugins("chroma", "musicbrainz")
fake_track = TrackInfo(title="Fake Track", track_id="rec-id-1")
mb_plugin = metadata_plugins.get_metadata_source("musicbrainz")
assert mb_plugin is not None
monkeypatch.setattr(
mb_plugin, "track_for_id", MagicMock(return_value=fake_track)
)
plugin = chroma.AcoustidPlugin()
item = _seed_acoustid_match()
result = list(plugin.item_candidates(item, artist="A", title="B"))
assert result == [fake_track]
mb_plugin.track_for_id.assert_called_with("rec-id-1")