diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 3759685719..0bda6cb732 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -19,13 +19,30 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} cache: poetry + - name: Install system dependencies + run: | + sudo apt update + sudo apt install --yes --no-install-recommends \ + ffmpeg \ + gobject-introspection \ + gstreamer1.0-plugins-base \ + imagemagick \ + libcairo2-dev \ + libgirepository-2.0-dev \ + mp3gain \ + pandoc \ + python3-gst-1.0 + - name: Install dependencies - run: poetry install + run: poetry install --extras=autobpm --extras=discogs --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate - name: Test env: INTEGRATION_TEST: 1 - run: poe test + LYRICS_UPDATED: 1 + run: | + poe docs + poe test - name: Check external links in docs run: poe check-docs-links @@ -34,7 +51,7 @@ jobs: if: ${{ failure() }} env: ZULIP_BOT_CREDENTIALS: ${{ secrets.ZULIP_BOT_CREDENTIALS }} - run: | + run: |- if [ -z "${ZULIP_BOT_CREDENTIALS}" ]; then echo "Skipping notify, ZULIP_BOT_CREDENTIALS is unset" exit 0 diff --git a/beets/test/helper.py b/beets/test/helper.py index 654de3e946..4312f81634 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -25,6 +25,7 @@ from __future__ import annotations +import importlib.util import os import os.path import shutil @@ -34,7 +35,7 @@ from contextlib import contextmanager from dataclasses import dataclass from enum import Enum -from functools import cached_property +from functools import cache, cached_property from pathlib import Path from tempfile import gettempdir, mkdtemp, mkstemp from typing import TYPE_CHECKING, Any, ClassVar @@ -61,6 +62,8 @@ if TYPE_CHECKING: from requests_mock.mocker import Mocker +RUNNING_IN_CI = os.environ.get("GITHUB_ACTIONS") == "true" + class LogCapture(logging.Handler): def __init__(self): @@ -98,6 +101,11 @@ def has_program(cmd, args=["--version"]): return True +@cache +def is_importable(modname: str) -> bool: + return bool(importlib.util.find_spec(modname)) + + def check_reflink_support(path: str) -> bool: try: import reflink @@ -107,6 +115,15 @@ def check_reflink_support(path: str) -> bool: return reflink.supported_at(path) +NEEDS_REFLINK = pytest.mark.skipif( + not check_reflink_support(gettempdir()), reason="need reflink" +) +NEEDS_FFPROBE = pytest.mark.skipif( + not has_program("ffprobe") and not RUNNING_IN_CI, + reason="ffprobe (ffmpeg) is not available", +) + + class ConfigMixin: @cached_property def config(self) -> beets.IncludeLazyConfig: @@ -122,11 +139,6 @@ def config(self) -> beets.IncludeLazyConfig: return config -NEEDS_REFLINK = pytest.mark.skipif( - not check_reflink_support(gettempdir()), reason="need reflink" -) - - class RunMixin: lib: Library diff --git a/docs/conf.py b/docs/conf.py index ec894931e9..a361b785e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,7 +74,14 @@ r"https://[^/]*fanart\.tv/", # blocks requests r"https://[^/]*fandom\.com/", # blocks requests r"https://imgur\.com/", # not accessible from the UK - r"https://www.discogs.com/settings/developers", # requires login + r"https://(www\.)?discogs.com.*", # blocks requests + r"https://essentia.upf.edu/", # times out in CI + r"https://flask.palletsprojects.com.*", # times out in CI + r"https://search.worldcat.org.*", # blocks requests + r"https://tidal.com.*", # blocks requests + r"https://www.tekstowo.pl/", # blocks requests + r"https://www.gnu.org.*", # sometimes unreachable + r"https://www.nongnu.org.*", # sometimes unreachable ] # Options for HTML output diff --git a/docs/faq.rst b/docs/faq.rst index 7f33992695..ed17f34a86 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -171,7 +171,7 @@ follow these guidelines when reporting an issue: - Most importantly: if beets is crashing, please `include the traceback `__. Tracebacks can be more readable if you put them - in a pastebin (e.g., `Gist `__ or `Hastebin + in a pastebin (e.g., `Gist `__ or `Hastebin `__), especially when communicating over IRC. - Turn on beets' debug output (using the -v option: for example, ``beet -v diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index c96b4a9f9b..aace4cffc4 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -221,7 +221,7 @@ To use the google image search backend you need to `register for a Google API key`_. Set the ``google_key`` configuration option to your key, then add ``google`` to the list of sources in your configuration. -.. _register for a google api key: https://console.developers.google.com. +.. _register for a google api key: https://console.cloud.google.com/apis/credentials Optionally, you can `define a custom search engine`_. Get your search engine's token and use it for your ``google_engine`` configuration option. The default diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index c6dd3d807c..ae83ffdc14 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -188,8 +188,8 @@ Activate Google Custom Search ----------------------------- You need to `register for a Google API key -`__. Set the ``google_API_key`` -configuration option to your key. +`__. Set the +``google_API_key`` configuration option to your key. Then add ``google`` to the list of sources in your configuration (or use default list, which includes it as long as you have an API key). If you use default diff --git a/test/conftest.py b/test/conftest.py index c083489a88..75a40bee47 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,22 +1,16 @@ -import importlib.util import inspect import os -from functools import cache import pytest from beets.autotag import Distance from beets.dbcore.query import Query from beets.test._common import DummyIO -from beets.test.helper import ConfigMixin +from beets.test.helper import RUNNING_IN_CI, ConfigMixin +from beets.test.helper import is_importable as check_import from beets.util import cached_classproperty -@cache -def _is_importable(modname: str) -> bool: - return bool(importlib.util.find_spec(modname)) - - def skip_marked_items(items: list[pytest.Item], marker_name: str, reason: str): for item in (i for i in items if i.get_closest_marker(marker_name)): test_name = item.nodeid.split("::", 1)[-1] @@ -41,7 +35,7 @@ def pytest_collection_modifyitems( force_ci = marker.kwargs.get("force_ci", True) if ( force_ci - and os.environ.get("GITHUB_ACTIONS") == "true" + and RUNNING_IN_CI # only apply this to our repository, to allow other projects to # run tests without installing all dependencies and os.environ.get("GITHUB_REPOSITORY", "") == "beetbox/beets" @@ -49,7 +43,7 @@ def pytest_collection_modifyitems( continue modname = marker.args[0] - if not _is_importable(modname): + if not check_import(modname): test_name = item.nodeid.split("::", 1)[-1] item.add_marker( pytest.mark.skip( @@ -134,4 +128,4 @@ def io( def is_importable(): """Fixture that provides a function to check if a module can be imported.""" - return _is_importable + return check_import diff --git a/test/plugins/test_art.py b/test/plugins/test_art.py index 9831abcbc8..7259a25c42 100644 --- a/test/plugins/test_art.py +++ b/test/plugins/test_art.py @@ -31,18 +31,28 @@ from beets.autotag import AlbumInfo, AlbumMatch, Distance from beets.library import Album from beets.test import _common -from beets.test.helper import FetchImageHelper, TestHelper +from beets.test.helper import ( + RUNNING_IN_CI, + FetchImageHelper, + TestHelper, + has_program, + is_importable, +) from beets.util import clean_module_tempdir, syspath from beets.util.artresizer import ArtResizer from beetsplug import fetchart -logger = logging.getLogger("beets.test_art") - - if TYPE_CHECKING: from collections.abc import Iterator, Sequence from unittest.mock import MagicMock +logger = logging.getLogger("beets.test_art") + +REQUIRES_ARTRESIZER = pytest.mark.skipif( + not (has_program("magick") or is_importable("PIL")) and not RUNNING_IN_CI, + reason="requires ImageMagick or Pillow", +) + class Settings(fetchart.FetchArtPlugin): """Used to pass settings to the ArtSources when the plugin isn't fully @@ -992,6 +1002,7 @@ def get_album_art(self): return self.plugin.art_for_album(Album(), [""], True) +@REQUIRES_ARTRESIZER class TestAlbumArtOperationConfiguration(AlbumArtOperationMixin): """Check that scale & filesize configuration is respected. @@ -1033,6 +1044,7 @@ def test_enforce_ratio_with_percent_margin(self): assert self.get_album_art() +@REQUIRES_ARTRESIZER class TestAlbumArtPerformOperation(AlbumArtOperationMixin): """Test that the art is resized and deinterlaced if necessary.""" diff --git a/test/plugins/test_chroma.py b/test/plugins/test_chroma.py index 2ee4ae0c26..4e42218685 100644 --- a/test/plugins/test_chroma.py +++ b/test/plugins/test_chroma.py @@ -20,7 +20,8 @@ from beets.autotag import AlbumInfo, TrackInfo from beets.library import Item from beets.test.helper import ImportHelper, IOMixin, PluginMixin -from beetsplug import chroma + +chroma = pytest.importorskip("beetsplug.chroma", exc_type=ImportError) TEST_TITLE_1 = "TEST_TITLE_1" TEST_TITLE_2 = "TEST_TITLE_2" diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index 232d98ff14..b197f338cd 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -29,6 +29,7 @@ from beets.exceptions import UserError from beets.test import _common from beets.test.helper import ( + NEEDS_FFPROBE, FetchImageHelper, ImportHelper, IOMixin, @@ -308,6 +309,7 @@ def test_embed_art_from_url_not_image( mediafile = MediaFile(syspath(item.path)) assert not mediafile.images + @NEEDS_FFPROBE def test_clearart_on_import_disabled(self): file_path = self.create_mediafile_fixture( images=["jpg"], target_dir=self.import_path @@ -320,6 +322,7 @@ def test_clearart_on_import_disabled(self): item = self.lib.items()[0] assert MediaFile(os.path.join(item.path)).images + @NEEDS_FFPROBE def test_clearart_on_import_enabled(self): file_path = self.create_mediafile_fixture( images=["jpg"], target_dir=self.import_path diff --git a/test/test_importer.py b/test/test_importer.py index e78a19bfae..b8d3528c0b 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -40,6 +40,7 @@ from beets.importer.tasks import albums_in_dir from beets.test import _common from beets.test.helper import ( + NEEDS_FFPROBE, NEEDS_REFLINK, AsIsImporterMixin, AutotagImportTestCase, @@ -518,6 +519,7 @@ def test_skip_does_not_add_track(self): assert not self.lib.items() + @NEEDS_FFPROBE def test_skip_non_album_dirs(self): assert (self.import_path / "album").exists() self.touch(b"cruft", dir_=self.import_dir) @@ -533,6 +535,7 @@ def test_unmatched_tracks_not_added(self): self.importer.run() assert len(self.lib.items()) == 1 + @NEEDS_FFPROBE def test_empty_directory_warning(self): import_dir = os.path.join(self.temp_dir, b"empty") self.touch(b"non-audio", dir_=import_dir) @@ -543,6 +546,7 @@ def test_empty_directory_warning(self): import_dir = displayable_path(import_dir) assert f"No files imported from {import_dir}" in logs + @NEEDS_FFPROBE def test_empty_directory_singleton_warning(self): import_dir = os.path.join(self.temp_dir, b"empty") self.touch(b"non-audio", dir_=import_dir) diff --git a/test/test_plugins.py b/test/test_plugins.py index 306c48d70f..9437ba51af 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -35,6 +35,7 @@ ) from beets.library import Item from beets.test.helper import ( + RUNNING_IN_CI, AutotagStub, ImportHelper, IOMixin, @@ -532,7 +533,7 @@ def cleanup(self): self.unload_plugins() @pytest.mark.skipif( - os.environ.get("GITHUB_ACTIONS") != "true", + not RUNNING_IN_CI, reason=( "Requires all dependencies to be installed, which we can't" " guarantee in the local environment." diff --git a/test/test_release.py b/test/test_release.py index e14d200fb1..45c545c792 100644 --- a/test/test_release.py +++ b/test/test_release.py @@ -1,17 +1,18 @@ """Tests for the release utils.""" -import os import shutil import sys import pytest +from beets.test.helper import RUNNING_IN_CI + release = pytest.importorskip("extra.release") pytestmark = pytest.mark.skipif( not ( - (os.environ.get("GITHUB_ACTIONS") == "true" and sys.platform != "win32") + (RUNNING_IN_CI and sys.platform != "win32") or bool(shutil.which("pandoc")) ), reason="pandoc isn't available", diff --git a/test/ui/commands/test_completion.py b/test/ui/commands/test_completion.py index edf4e57a0a..e26442b908 100644 --- a/test/ui/commands/test_completion.py +++ b/test/ui/commands/test_completion.py @@ -5,7 +5,7 @@ import pytest from beets.test import _common -from beets.test.helper import IOMixin, has_program +from beets.test.helper import RUNNING_IN_CI, IOMixin, has_program from beets.ui.commands.completion import BASH_COMPLETION_PATHS from beets.util import syspath @@ -13,7 +13,7 @@ @pytest.mark.xfail( - os.environ.get("GITHUB_ACTIONS") == "true" and sys.platform == "linux", + RUNNING_IN_CI and sys.platform == "linux", reason="Completion is for some reason unhappy on Ubuntu 24.04 in CI", ) class CompletionTest(IOMixin, TestPluginTestCase):