Skip to content

Commit 78c3611

Browse files
authored
Added a load all plugins test. (#5878)
Adds a test that loads all available plugins in the beetsplug namespace. This came up during #5876.
2 parents d8d227e + 3f23f35 commit 78c3611

File tree

4 files changed

+68
-5
lines changed

4 files changed

+68
-5
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ jobs:
5252
- if: ${{ env.IS_MAIN_PYTHON != 'true' }}
5353
name: Test without coverage
5454
run: |
55-
poetry install --extras=autobpm --extras=lyrics --extras=embedart
55+
poetry install --extras=autobpm --extras=lyrics --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
5656
poe test
5757
5858
- if: ${{ env.IS_MAIN_PYTHON == 'true' }}
5959
name: Test with coverage
6060
env:
6161
LYRICS_UPDATED: ${{ steps.lyrics-update.outputs.any_changed }}
6262
run: |
63-
poetry install --extras=autobpm --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart
63+
poetry install --extras=autobpm --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate
6464
poe docs
6565
poe test-with-coverage
6666

beetsplug/absubmit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import hashlib
1919
import json
2020
import os
21+
import shutil
2122
import subprocess
2223
import tempfile
23-
from distutils.spawn import find_executable
2424

2525
import requests
2626

@@ -84,7 +84,7 @@ def __init__(self):
8484

8585
# Get the executable location on the system, which we need
8686
# to calculate the SHA-1 hash.
87-
self.extractor = find_executable(self.extractor)
87+
self.extractor = shutil.which(self.extractor)
8888

8989
# Calculate extractor hash.
9090
self.extractor_sha = hashlib.sha1()

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Other changes:
102102
case is shown on separate lines.
103103
* Refactored library.py file by splitting it into multiple modules within the
104104
beets/library directory.
105+
* Added a test to check that all plugins can be imported without errors.
105106

106107
2.3.1 (May 14, 2025)
107108
--------------------

test/test_plugins.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
# included in all copies or substantial portions of the Software.
1414

1515

16+
import importlib
1617
import itertools
18+
import logging
1719
import os
20+
import pkgutil
21+
import sys
1822
from unittest.mock import ANY, Mock, patch
1923

2024
import pytest
@@ -30,7 +34,12 @@
3034
)
3135
from beets.library import Item
3236
from beets.test import helper
33-
from beets.test.helper import AutotagStub, ImportHelper, TerminalImportMixin
37+
from beets.test.helper import (
38+
AutotagStub,
39+
ImportHelper,
40+
PluginMixin,
41+
TerminalImportMixin,
42+
)
3443
from beets.test.helper import PluginTestCase as BasePluginTestCase
3544
from beets.util import displayable_path, syspath
3645

@@ -531,3 +540,56 @@ def foo(self, session, task):
531540
self.mock_input_options.assert_called_once_with(
532541
opts, default="a", require=ANY
533542
)
543+
544+
545+
def get_available_plugins():
546+
"""Get all available plugins in the beetsplug namespace."""
547+
namespace_pkg = importlib.import_module("beetsplug")
548+
549+
return [
550+
m.name
551+
for m in pkgutil.iter_modules(namespace_pkg.__path__)
552+
if not m.name.startswith("_")
553+
]
554+
555+
556+
class TestImportAllPlugins(PluginMixin):
557+
def unimport_plugins(self):
558+
"""Unimport plugins before each test to avoid conflicts."""
559+
self.unload_plugins()
560+
for mod in list(sys.modules):
561+
if mod.startswith("beetsplug."):
562+
del sys.modules[mod]
563+
564+
@pytest.fixture(autouse=True)
565+
def cleanup(self):
566+
"""Ensure plugins are unimported before and after each test."""
567+
self.unimport_plugins()
568+
yield
569+
self.unimport_plugins()
570+
571+
@pytest.mark.skipif(
572+
os.environ.get("GITHUB_ACTIONS") != "true",
573+
reason="Requires all dependencies to be installed, "
574+
+ "which we can't guarantee in the local environment.",
575+
)
576+
@pytest.mark.parametrize("plugin_name", get_available_plugins())
577+
def test_import_plugin(self, caplog, plugin_name): #
578+
"""Test that a plugin is importable without an error using the
579+
load_plugins function."""
580+
581+
# skip gstreamer plugins on windows
582+
gstreamer_plugins = ["bpd", "replaygain"]
583+
if sys.platform == "win32" and plugin_name in gstreamer_plugins:
584+
pytest.xfail("GStreamer is not available on Windows: {plugin_name}")
585+
586+
caplog.set_level(logging.WARNING)
587+
caplog.clear()
588+
plugins.load_plugins([plugin_name])
589+
590+
# Check for warnings, is a bit hacky but we can make full use of the beets
591+
# load_plugins code that way
592+
assert len(caplog.records) == 0, (
593+
f"Plugin '{plugin_name}' has issues during import. ",
594+
caplog.records,
595+
)

0 commit comments

Comments
 (0)