mirror of
https://github.com/beetbox/beets.git
synced 2026-01-16 05:02:28 +01:00
Make sure tests do not leave any temp files behind (#5345)
Fixes #5229, is part of #5361 and relates to #5285. I have to admit thsi was a fairly tough task - I initially assumed that the problem lies with how the tests are setup, and that we're probably missing some `teardown_beets` calls here and there. Unfortunately, it was not so simple. I came across several issues that gave rise to leftover temporary files: 1. `fetchart`, `artresizer` and `play` handling of temporary files. These plugins created isolated temporary files outside of the directories that tests clean up. You will find I added a couple of functions (namely `get_module_tempdir`) that force these plugins to create files in directories determined by their module names. This way we can clean up after them using the new `CleanupModulesMixin`. 2. Tests that ran temporary directories setup twice, running `_common.TestCase.setUp` and `test.helper.TestHelper.setup_beets`. Both of these ran `self.temp_dir = mkdtemp()`, therefore the directories created by the initial setup persisted since those have been overridden and thus unreachable in the teardown. Here, I removed the `setUp` calls, see - `test/plugins/test_embedart.py` - `test/test_importer.py` - and `test/test_plugins.py` where `setup_beets` was called twice 3. `test/test_config_command.py` attempted to manage the temporary directory by itself, where I found that `tearDown` failed to remove the directory for four tests. Could not figure out the cause, and found that delegating this task to `TestHelper` fixed the issue. 4. Mediafile fixture removal depended on calling `remove_mediafile_fixtures` method, which `test/plugins/test_zero.py` failed to do. I made the fixtures to be created within the same `temp_dir` directory that gets removed in the teardown, so now they are taken care of automatically. In summary, see the test modules that left files behind: ``` Temp files created by test/__init__.py Temp files created by test/plugins/__init__.py Temp files created by test/plugins/lyrics_download_samples.py Temp files created by test/plugins/test_acousticbrainz.py Temp files created by test/plugins/test_advancedrewrite.py Temp files created by test/plugins/test_albumtypes.py Temp files created by test/plugins/test_art.py /tmp/tmp11nicahe.jpg /tmp/tmp1bjmodum.png /tmp/tmped7nhls4.jpg /tmp/tmpflnzr9wz.jpg /tmp/tmpjngkauqs.png /tmp/tmpkzy9mn6t.jpg /tmp/tmpph_wmuea.jpg /tmp/tmps6gk58i_.jpg /tmp/tmpz2eji_o4.jpg Temp files created by test/plugins/test_aura.py Temp files created by test/plugins/test_bareasc.py /tmp/tmphl3kzhug /tmp/tmpnh2q6v02 /tmp/tmpppw5qrhz Temp files created by test/plugins/test_beatport.py Temp files created by test/plugins/test_bucket.py Temp files created by test/plugins/test_convert.py Temp files created by test/plugins/test_discogs.py Temp files created by test/plugins/test_edit.py Temp files created by test/plugins/test_embedart.py /tmp/tmp1ayvqzhx /tmp/tmp58k6mdfx.jpg /tmp/tmp64c2lqiv /tmp/tmp6nar4kr5 /tmp/tmp6u0d5dex /tmp/tmpacoq7w_f /tmp/tmpajnr_sxr /tmp/tmpasj16beh /tmp/tmpboyaixb5 /tmp/tmpcrmcyt5r /tmp/tmpdomje5g3 /tmp/tmplu3o6t6g /tmp/tmpns_xvkns /tmp/tmpo87o1h6o.jpg /tmp/tmpqem39h_j /tmp/tmprlzm18pb /tmp/tmpt22v4u6x /tmp/tmptp3rxdgv Temp files created by test/plugins/test_embyupdate.py Temp files created by test/plugins/test_export.py Temp files created by test/plugins/test_fetchart.py Temp files created by test/plugins/test_filefilter.py Temp files created by test/plugins/test_ftintitle.py Temp files created by test/plugins/test_hook.py Temp files created by test/plugins/test_ihate.py Temp files created by test/plugins/test_importadded.py Temp files created by test/plugins/test_importfeeds.py Temp files created by test/plugins/test_info.py Temp files created by test/plugins/test_ipfs.py Temp files created by test/plugins/test_keyfinder.py Temp files created by test/plugins/test_lastgenre.py Temp files created by test/plugins/test_limit.py Temp files created by test/plugins/test_lyrics.py Temp files created by test/plugins/test_mbsubmit.py Temp files created by test/plugins/test_mbsync.py Temp files created by test/plugins/test_mpdstats.py Temp files created by test/plugins/test_parentwork.py Temp files created by test/plugins/test_permissions.py Temp files created by test/plugins/test_player.py Temp files created by test/plugins/test_playlist.py Temp files created by test/plugins/test_play.py /tmp/tmp6ohknmve.m3u /tmp/tmp8rw2z_j4.m3u /tmp/tmp9vi27ypx.m3u /tmp/tmpa_s66jh8.m3u /tmp/tmpb7h3cn3n.m3u /tmp/tmpexbmqvry.m3u /tmp/tmpinbqrt80.m3u /tmp/tmpql02hax5.m3u /tmp/tmpvbdzprsf.m3u /tmp/tmpzipim36x.m3u Temp files created by test/plugins/test_plexupdate.py Temp files created by test/plugins/test_plugin_mediafield.py Temp files created by test/plugins/test_random.py Temp files created by test/plugins/test_replaygain.py Temp files created by test/plugins/test_smartplaylist.py Temp files created by test/plugins/test_spotify.py Temp files created by test/plugins/test_subsonicupdate.py Temp files created by test/plugins/test_the.py Temp files created by test/plugins/test_thumbnails.py Temp files created by test/plugins/test_types_plugin.py Temp files created by test/plugins/test_web.py Temp files created by test/plugins/test_zero.py /tmp/tmp3ub9xmzy Temp files created by test/rsrc/beetsplug/test.py Temp files created by test/rsrc/convert_stub.py Temp files created by test/testall.py Temp files created by test/test_art_resize.py /tmp/tmp3p7p60ih.jpg /tmp/tmp8exclgit.jpg /tmp/tmpkrrjsitl.jpg /tmp/tmpw6n8ee8e.jpg /tmp/tmpygws_0aw.jpg Temp files created by test/test_autotag.py Temp files created by test/test_config_command.py /tmp/tmp333f0r2j /tmp/tmphr356z5r /tmp/tmporp4rag2 /tmp/tmpy7sjqdsw Temp files created by test/test_datequery.py Temp files created by test/test_dbcore.py Temp files created by test/test_files.py Temp files created by test/test_hidden.py Temp files created by test/test_importer.py /tmp/tmp0m363gfb /tmp/tmp2n3i13mc /tmp/tmpxk3v304s Temp files created by test/test_library.py Temp files created by test/test_logging.py Temp files created by test/test_m3ufile.py Temp files created by test/test_mb.py Temp files created by test/test_metasync.py Temp files created by test/test_pipeline.py Temp files created by test/test_plugins.py /tmp/tmp6pxhx67u /tmp/tmpb8pqi9ui /tmp/tmpcx_658g7 /tmp/tmp_giqb9jz /tmp/tmpgm9xk94_ /tmp/tmpk60l6bt3 /tmp/tmpqoj4la68 /tmp/tmptcdu20rp /tmp/tmpvr7k5shn /tmp/tmpwnfnzs91 Temp files created by test/test_query.py Temp files created by test/test_sort.py Temp files created by test/test_template.py Temp files created by test/test_ui_commands.py /tmp/tmpns2u94w6 Temp files created by test/test_ui_importer.py Temp files created by test/test_ui_init.py Temp files created by test/test_ui.py Temp files created by test/test_util.py Temp files created by test/test_vfs.py ``` And that's what we have right now: ``` Temp files created by test/__init__.py Temp files created by test/plugins/__init__.py Temp files created by test/plugins/lyrics_download_samples.py Temp files created by test/plugins/test_acousticbrainz.py Temp files created by test/plugins/test_advancedrewrite.py Temp files created by test/plugins/test_albumtypes.py Temp files created by test/plugins/test_art.py Temp files created by test/plugins/test_aura.py Temp files created by test/plugins/test_bareasc.py Temp files created by test/plugins/test_beatport.py Temp files created by test/plugins/test_bucket.py Temp files created by test/plugins/test_convert.py Temp files created by test/plugins/test_discogs.py Temp files created by test/plugins/test_edit.py Temp files created by test/plugins/test_embedart.py Temp files created by test/plugins/test_embyupdate.py Temp files created by test/plugins/test_export.py Temp files created by test/plugins/test_fetchart.py Temp files created by test/plugins/test_filefilter.py Temp files created by test/plugins/test_ftintitle.py Temp files created by test/plugins/test_hook.py Temp files created by test/plugins/test_ihate.py Temp files created by test/plugins/test_importadded.py Temp files created by test/plugins/test_importfeeds.py Temp files created by test/plugins/test_info.py Temp files created by test/plugins/test_ipfs.py Temp files created by test/plugins/test_keyfinder.py Temp files created by test/plugins/test_lastgenre.py Temp files created by test/plugins/test_limit.py Temp files created by test/plugins/test_lyrics.py Temp files created by test/plugins/test_mbsubmit.py Temp files created by test/plugins/test_mbsync.py Temp files created by test/plugins/test_mpdstats.py Temp files created by test/plugins/test_parentwork.py Temp files created by test/plugins/test_permissions.py Temp files created by test/plugins/test_player.py Temp files created by test/plugins/test_playlist.py Temp files created by test/plugins/test_play.py Temp files created by test/plugins/test_plexupdate.py Temp files created by test/plugins/test_plugin_mediafield.py Temp files created by test/plugins/test_random.py Temp files created by test/plugins/test_replaygain.py Temp files created by test/plugins/test_smartplaylist.py Temp files created by test/plugins/test_spotify.py Temp files created by test/plugins/test_subsonicupdate.py Temp files created by test/plugins/test_the.py Temp files created by test/plugins/test_thumbnails.py Temp files created by test/plugins/test_types_plugin.py Temp files created by test/plugins/test_web.py Temp files created by test/plugins/test_zero.py Temp files created by test/rsrc/beetsplug/test.py Temp files created by test/rsrc/convert_stub.py Temp files created by test/testall.py Temp files created by test/test_art_resize.py Temp files created by test/test_autotag.py Temp files created by test/test_config_command.py Temp files created by test/test_datequery.py Temp files created by test/test_dbcore.py Temp files created by test/test_files.py Temp files created by test/test_hidden.py Temp files created by test/test_importer.py Temp files created by test/test_library.py Temp files created by test/test_logging.py Temp files created by test/test_m3ufile.py Temp files created by test/test_mb.py Temp files created by test/test_metasync.py Temp files created by test/test_pipeline.py Temp files created by test/test_plugins.py Temp files created by test/test_query.py Temp files created by test/test_sort.py Temp files created by test/test_template.py Temp files created by test/test_ui_commands.py Temp files created by test/test_ui_importer.py Temp files created by test/test_ui_init.py Temp files created by test/test_ui.py Temp files created by test/test_util.py Temp files created by test/test_vfs.py ``` Note that the command which provides the output is now available through `poe`.
This commit is contained in:
commit
1163645604
17 changed files with 181 additions and 136 deletions
|
|
@ -29,6 +29,7 @@ information or mock the environment.
|
|||
- The `TestHelper` class encapsulates various fixtures that can be set up.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
|
@ -39,7 +40,9 @@ from contextlib import contextmanager
|
|||
from enum import Enum
|
||||
from io import StringIO
|
||||
from tempfile import mkdtemp, mkstemp
|
||||
from typing import ClassVar
|
||||
|
||||
import responses
|
||||
from mediafile import Image, MediaFile
|
||||
|
||||
import beets
|
||||
|
|
@ -49,7 +52,12 @@ from beets.autotag.hooks import AlbumInfo, TrackInfo
|
|||
from beets.library import Album, Item, Library
|
||||
from beets.test import _common
|
||||
from beets.ui.commands import TerminalImportSession
|
||||
from beets.util import MoveOperation, bytestring_path, syspath
|
||||
from beets.util import (
|
||||
MoveOperation,
|
||||
bytestring_path,
|
||||
clean_module_tempdir,
|
||||
syspath,
|
||||
)
|
||||
|
||||
|
||||
class LogCapture(logging.Handler):
|
||||
|
|
@ -397,18 +405,14 @@ class TestHelper:
|
|||
return self.lib.add_album(items)
|
||||
|
||||
def create_mediafile_fixture(self, ext="mp3", images=[]):
|
||||
"""Copies a fixture mediafile with the extension to a temporary
|
||||
location and returns the path.
|
||||
|
||||
It keeps track of the created locations and will delete the with
|
||||
`remove_mediafile_fixtures()`
|
||||
"""Copy a fixture mediafile with the extension to `temp_dir`.
|
||||
|
||||
`images` is a subset of 'png', 'jpg', and 'tiff'. For each
|
||||
specified extension a cover art image is added to the media
|
||||
file.
|
||||
"""
|
||||
src = os.path.join(_common.RSRC, util.bytestring_path("full." + ext))
|
||||
handle, path = mkstemp()
|
||||
handle, path = mkstemp(dir=self.temp_dir)
|
||||
path = bytestring_path(path)
|
||||
os.close(handle)
|
||||
shutil.copyfile(syspath(src), syspath(path))
|
||||
|
|
@ -424,17 +428,8 @@ class TestHelper:
|
|||
mediafile.images = imgs
|
||||
mediafile.save()
|
||||
|
||||
if not hasattr(self, "_mediafile_fixtures"):
|
||||
self._mediafile_fixtures = []
|
||||
self._mediafile_fixtures.append(path)
|
||||
|
||||
return path
|
||||
|
||||
def remove_mediafile_fixtures(self):
|
||||
if hasattr(self, "_mediafile_fixtures"):
|
||||
for path in self._mediafile_fixtures:
|
||||
os.remove(syspath(path))
|
||||
|
||||
def _get_item_count(self):
|
||||
if not hasattr(self, "__item_count"):
|
||||
count = 0
|
||||
|
|
@ -925,3 +920,39 @@ class AutotagStub:
|
|||
albumtype="soundtrack",
|
||||
data_source="match_source",
|
||||
)
|
||||
|
||||
|
||||
class FetchImageHelper:
|
||||
"""Helper mixin for mocking requests when fetching images
|
||||
with remote art sources.
|
||||
"""
|
||||
|
||||
@responses.activate
|
||||
def run(self, *args, **kwargs):
|
||||
super().run(*args, **kwargs)
|
||||
|
||||
IMAGEHEADER = {
|
||||
"image/jpeg": b"\x00" * 6 + b"JFIF",
|
||||
"image/png": b"\211PNG\r\n\032\n",
|
||||
}
|
||||
|
||||
def mock_response(self, url, content_type="image/jpeg", file_type=None):
|
||||
if file_type is None:
|
||||
file_type = content_type
|
||||
responses.add(
|
||||
responses.GET,
|
||||
url,
|
||||
content_type=content_type,
|
||||
# imghdr reads 32 bytes
|
||||
body=self.IMAGEHEADER.get(file_type, b"").ljust(32, b"\x00"),
|
||||
)
|
||||
|
||||
|
||||
class CleanupModulesMixin:
|
||||
modules: ClassVar[tuple[str, ...]]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
"""Remove files created by the plugin."""
|
||||
for module in cls.modules:
|
||||
clean_module_tempdir(module)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""Miscellaneous utility functions."""
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import fnmatch
|
||||
|
|
@ -26,9 +27,11 @@ import sys
|
|||
import tempfile
|
||||
import traceback
|
||||
from collections import Counter, namedtuple
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
from logging import Logger
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
AnyStr,
|
||||
|
|
@ -58,6 +61,7 @@ MAX_FILENAME_LENGTH = 200
|
|||
WINDOWS_MAGIC_PREFIX = "\\\\?\\"
|
||||
T = TypeVar("T")
|
||||
Bytes_or_String: TypeAlias = Union[str, bytes]
|
||||
PathLike = Union[str, bytes, Path]
|
||||
|
||||
|
||||
class HumanReadableException(Exception):
|
||||
|
|
@ -1076,3 +1080,46 @@ class cached_classproperty: # noqa: N801
|
|||
self.cache[owner] = self.getter(owner)
|
||||
|
||||
return self.cache[owner]
|
||||
|
||||
|
||||
def get_module_tempdir(module: str) -> Path:
|
||||
"""Return the temporary directory for the given module.
|
||||
|
||||
The directory is created within the `/tmp/beets/<module>` directory on
|
||||
Linux (or the equivalent temporary directory on other systems).
|
||||
|
||||
Dots in the module name are replaced by underscores.
|
||||
"""
|
||||
module = module.replace("beets.", "").replace(".", "_")
|
||||
return Path(tempfile.gettempdir()) / "beets" / module
|
||||
|
||||
|
||||
def clean_module_tempdir(module: str) -> None:
|
||||
"""Clean the temporary directory for the given module."""
|
||||
tempdir = get_module_tempdir(module)
|
||||
shutil.rmtree(tempdir, ignore_errors=True)
|
||||
with suppress(OSError):
|
||||
# remove parent (/tmp/beets) directory if it is empty
|
||||
tempdir.parent.rmdir()
|
||||
|
||||
|
||||
def get_temp_filename(
|
||||
module: str,
|
||||
prefix: str = "",
|
||||
path: PathLike | None = None,
|
||||
suffix: str = "",
|
||||
) -> bytes:
|
||||
"""Return temporary filename for the given module and prefix.
|
||||
|
||||
The filename starts with the given `prefix`.
|
||||
If 'suffix' is given, it is used a the file extension.
|
||||
If 'path' is given, we use the same suffix.
|
||||
"""
|
||||
if not suffix and path:
|
||||
suffix = Path(os.fsdecode(path)).suffix
|
||||
|
||||
tempdir = get_module_tempdir(module)
|
||||
tempdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_, filename = tempfile.mkstemp(dir=tempdir, prefix=prefix, suffix=suffix)
|
||||
return bytestring_path(filename)
|
||||
|
|
|
|||
|
|
@ -22,11 +22,10 @@ import platform
|
|||
import re
|
||||
import subprocess
|
||||
from itertools import chain
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from beets import logging, util
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util import displayable_path, get_temp_filename, syspath
|
||||
|
||||
PROXY_URL = "https://images.weserv.nl/"
|
||||
|
||||
|
|
@ -48,15 +47,6 @@ def resize_url(url, maxwidth, quality=0):
|
|||
return "{}?{}".format(PROXY_URL, urlencode(params))
|
||||
|
||||
|
||||
def temp_file_for(path):
|
||||
"""Return an unused filename with the same extension as the
|
||||
specified path.
|
||||
"""
|
||||
ext = os.path.splitext(path)[1]
|
||||
with NamedTemporaryFile(suffix=os.fsdecode(ext), delete=False) as f:
|
||||
return bytestring_path(f.name)
|
||||
|
||||
|
||||
class LocalBackendNotAvailableError(Exception):
|
||||
pass
|
||||
|
||||
|
|
@ -141,7 +131,9 @@ class IMBackend(LocalBackend):
|
|||
Use the ``magick`` program or ``convert`` on older versions. Return
|
||||
the output path of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "resize_IM_", path_in)
|
||||
|
||||
log.debug(
|
||||
"artresizer: ImageMagick resizing {0} to {1}",
|
||||
displayable_path(path_in),
|
||||
|
|
@ -208,7 +200,8 @@ class IMBackend(LocalBackend):
|
|||
return None
|
||||
|
||||
def deinterlace(self, path_in, path_out=None):
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in)
|
||||
|
||||
cmd = self.convert_cmd + [
|
||||
syspath(path_in, prefix=False),
|
||||
|
|
@ -366,7 +359,9 @@ class PILBackend(LocalBackend):
|
|||
"""Resize using Python Imaging Library (PIL). Return the output path
|
||||
of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "resize_PIL_", path_in)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
log.debug(
|
||||
|
|
@ -442,7 +437,9 @@ class PILBackend(LocalBackend):
|
|||
return None
|
||||
|
||||
def deinterlace(self, path_in, path_out=None):
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
if not path_out:
|
||||
path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -19,14 +19,13 @@ import os
|
|||
import re
|
||||
from collections import OrderedDict
|
||||
from contextlib import closing
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import confuse
|
||||
import requests
|
||||
from mediafile import image_mime_type
|
||||
|
||||
from beets import config, importer, plugins, ui, util
|
||||
from beets.util import bytestring_path, sorted_walk, syspath
|
||||
from beets.util import bytestring_path, get_temp_filename, sorted_walk, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
|
||||
try:
|
||||
|
|
@ -412,17 +411,17 @@ class RemoteArtSource(ArtSource):
|
|||
ext,
|
||||
)
|
||||
|
||||
suffix = os.fsdecode(ext)
|
||||
with NamedTemporaryFile(suffix=suffix, delete=False) as fh:
|
||||
filename = get_temp_filename(__name__, suffix=ext.decode())
|
||||
with open(filename, "wb") as fh:
|
||||
# write the first already loaded part of the image
|
||||
fh.write(header)
|
||||
# download the remaining part of the image
|
||||
for chunk in data:
|
||||
fh.write(chunk)
|
||||
self._log.debug(
|
||||
"downloaded art to: {0}", util.displayable_path(fh.name)
|
||||
"downloaded art to: {0}", util.displayable_path(filename)
|
||||
)
|
||||
candidate.path = util.bytestring_path(fh.name)
|
||||
candidate.path = util.bytestring_path(filename)
|
||||
return
|
||||
|
||||
except (OSError, requests.RequestException, TypeError) as exc:
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@
|
|||
import shlex
|
||||
import subprocess
|
||||
from os.path import relpath
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from beets import config, ui, util
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.ui import Subcommand
|
||||
from beets.ui.commands import PromptChoice
|
||||
from beets.util import get_temp_filename
|
||||
|
||||
# Indicate where arguments should be inserted into the command string.
|
||||
# If this is missing, they're placed at the end.
|
||||
|
|
@ -194,15 +194,15 @@ class PlayPlugin(BeetsPlugin):
|
|||
def _create_tmp_playlist(self, paths_list):
|
||||
"""Create a temporary .m3u file. Return the filename."""
|
||||
utf8_bom = config["play"]["bom"].get(bool)
|
||||
m3u = NamedTemporaryFile("wb", suffix=".m3u", delete=False)
|
||||
filename = get_temp_filename(__name__, suffix=".m3u")
|
||||
with open(filename, "wb") as m3u:
|
||||
if utf8_bom:
|
||||
m3u.write(b"\xEF\xBB\xBF")
|
||||
|
||||
if utf8_bom:
|
||||
m3u.write(b"\xEF\xBB\xBF")
|
||||
for item in paths_list:
|
||||
m3u.write(item + b"\n")
|
||||
|
||||
for item in paths_list:
|
||||
m3u.write(item + b"\n")
|
||||
m3u.close()
|
||||
return m3u.name
|
||||
return filename
|
||||
|
||||
def before_choose_candidate_listener(self, session, task):
|
||||
"""Append a "Play" choice to the interactive importer prompt."""
|
||||
|
|
|
|||
|
|
@ -230,6 +230,21 @@ env.OPTS = """
|
|||
--cov-context=test
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.check-temp-files]
|
||||
help = "Run each test module one by one and check for leftover temp files"
|
||||
shell = """
|
||||
setopt nullglob
|
||||
for file in test/**/*.py; do
|
||||
print Temp files created by $file && poe test $file &>/dev/null
|
||||
tempfiles=(/tmp/**/tmp* /tmp/beets/**/*)
|
||||
if (( $#tempfiles )); then
|
||||
print -l $'\t'$^tempfiles
|
||||
rm -r --interactive=never $tempfiles &>/dev/null
|
||||
fi
|
||||
done
|
||||
"""
|
||||
interpreter = "zsh"
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
target-version = ["py38", "py39", "py310", "py311"]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import responses
|
|||
from beets import config, importer, library, logging, util
|
||||
from beets.autotag import AlbumInfo, AlbumMatch
|
||||
from beets.test import _common
|
||||
from beets.test.helper import capture_log
|
||||
from beets.test.helper import CleanupModulesMixin, FetchImageHelper, capture_log
|
||||
from beets.util import syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
from beetsplug import fetchart
|
||||
|
|
@ -44,36 +44,16 @@ class Settings:
|
|||
setattr(self, k, v)
|
||||
|
||||
|
||||
class UseThePlugin(_common.TestCase):
|
||||
class UseThePlugin(CleanupModulesMixin, _common.TestCase):
|
||||
modules = (fetchart.__name__, ArtResizer.__module__)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.plugin = fetchart.FetchArtPlugin()
|
||||
|
||||
|
||||
class FetchImageHelper(_common.TestCase):
|
||||
"""Helper mixin for mocking requests when fetching images
|
||||
with remote art sources.
|
||||
"""
|
||||
|
||||
@responses.activate
|
||||
def run(self, *args, **kwargs):
|
||||
super().run(*args, **kwargs)
|
||||
|
||||
IMAGEHEADER = {
|
||||
"image/jpeg": b"\x00" * 6 + b"JFIF",
|
||||
"image/png": b"\211PNG\r\n\032\n",
|
||||
}
|
||||
|
||||
def mock_response(self, url, content_type="image/jpeg", file_type=None):
|
||||
if file_type is None:
|
||||
file_type = content_type
|
||||
responses.add(
|
||||
responses.GET,
|
||||
url,
|
||||
content_type=content_type,
|
||||
# imghdr reads 32 bytes
|
||||
body=self.IMAGEHEADER.get(file_type, b"").ljust(32, b"\x00"),
|
||||
)
|
||||
class FetchImageTestCase(FetchImageHelper, UseThePlugin):
|
||||
pass
|
||||
|
||||
|
||||
class CAAHelper:
|
||||
|
|
@ -212,7 +192,7 @@ class CAAHelper:
|
|||
)
|
||||
|
||||
|
||||
class FetchImageTest(FetchImageHelper, UseThePlugin):
|
||||
class FetchImageTest(FetchImageTestCase):
|
||||
URL = "http://example.com/test.jpg"
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -293,7 +273,7 @@ class FSArtTest(UseThePlugin):
|
|||
self.assertEqual(candidates, paths)
|
||||
|
||||
|
||||
class CombinedTest(FetchImageHelper, UseThePlugin, CAAHelper):
|
||||
class CombinedTest(FetchImageTestCase, CAAHelper):
|
||||
ASIN = "xxxx"
|
||||
MBID = "releaseid"
|
||||
AMAZON_URL = "https://images.amazon.com/images/P/{}.01.LZZZZZZZ.jpg".format(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ class BareascPluginTest(unittest.TestCase, TestHelper):
|
|||
self.add_item(title="without umlaut or e", artist="Bruggen")
|
||||
self.add_item(title="without umlaut with e", artist="Brueggen")
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
|
||||
def test_bareasc_search(self):
|
||||
test_cases = [
|
||||
(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import os.path
|
|||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from test.plugins.test_art import FetchImageHelper
|
||||
from test.test_art_resize import DummyIMBackend
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ from mediafile import MediaFile
|
|||
|
||||
from beets import art, config, logging, ui
|
||||
from beets.test import _common
|
||||
from beets.test.helper import TestHelper
|
||||
from beets.test.helper import FetchImageHelper, TestHelper
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util.artresizer import ArtResizer
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ class EmbedartCliTest(TestHelper, FetchImageHelper):
|
|||
abbey_differentpath = os.path.join(_common.RSRC, b"abbey-different.jpg")
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.io = _common.DummyIO()
|
||||
self.io.install()
|
||||
self.setup_beets() # Converter is threaded
|
||||
self.load_plugins("embedart")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class InfoTest(unittest.TestCase, TestHelper):
|
|||
self.assertIn("disctitle: DDD", out)
|
||||
self.assertIn("genres: a; b; c", out)
|
||||
self.assertNotIn("composer:", out)
|
||||
self.remove_mediafile_fixtures()
|
||||
|
||||
def test_item_query(self):
|
||||
item1, item2 = self.add_item_fixtures(count=2)
|
||||
|
|
@ -88,7 +87,6 @@ class InfoTest(unittest.TestCase, TestHelper):
|
|||
self.assertIn("album: AAA", out)
|
||||
self.assertIn("tracktotal: 5", out)
|
||||
self.assertIn("title: [various]", out)
|
||||
self.remove_mediafile_fixtures()
|
||||
|
||||
def test_collect_item_and_path_with_multi_values(self):
|
||||
path = self.create_mediafile_fixture()
|
||||
|
|
@ -116,7 +114,6 @@ class InfoTest(unittest.TestCase, TestHelper):
|
|||
self.assertIn("title: [various]", out)
|
||||
self.assertIn("albumartists: [various]", out)
|
||||
self.assertIn("artists: Artist A; Artist Z", out)
|
||||
self.remove_mediafile_fixtures()
|
||||
|
||||
def test_custom_format(self):
|
||||
self.add_item_fixtures()
|
||||
|
|
|
|||
|
|
@ -20,13 +20,16 @@ import sys
|
|||
import unittest
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from beets.test.helper import TestHelper, control_stdin
|
||||
from beets.test.helper import CleanupModulesMixin, TestHelper, control_stdin
|
||||
from beets.ui import UserError
|
||||
from beets.util import open_anything
|
||||
from beetsplug.play import PlayPlugin
|
||||
|
||||
|
||||
@patch("beetsplug.play.util.interactive_open")
|
||||
class PlayPluginTest(unittest.TestCase, TestHelper):
|
||||
class PlayPluginTest(CleanupModulesMixin, unittest.TestCase, TestHelper):
|
||||
modules = (PlayPlugin.__module__,)
|
||||
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
self.load_plugins("play")
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from shlex import quote
|
||||
|
||||
|
|
@ -72,7 +70,10 @@ class PlaylistTestHelper(helper.TestHelper):
|
|||
self.lib.add(i3)
|
||||
self.lib.add_album([i3])
|
||||
|
||||
self.playlist_dir = tempfile.mkdtemp()
|
||||
self.playlist_dir = os.path.join(
|
||||
os.fsdecode(self.temp_dir), "playlists"
|
||||
)
|
||||
os.makedirs(self.playlist_dir)
|
||||
self.config["directory"] = self.music_dir
|
||||
self.config["playlist"]["playlist_dir"] = self.playlist_dir
|
||||
|
||||
|
|
@ -84,7 +85,6 @@ class PlaylistTestHelper(helper.TestHelper):
|
|||
|
||||
def tearDown(self):
|
||||
self.unload_plugins()
|
||||
shutil.rmtree(self.playlist_dir)
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import unittest
|
|||
from unittest.mock import patch
|
||||
|
||||
from beets.test import _common
|
||||
from beets.test.helper import TestHelper
|
||||
from beets.test.helper import CleanupModulesMixin, TestHelper
|
||||
from beets.util import command_output, syspath
|
||||
from beets.util.artresizer import IMBackend, PILBackend
|
||||
|
||||
|
|
@ -48,9 +48,11 @@ class DummyPILBackend(PILBackend):
|
|||
pass
|
||||
|
||||
|
||||
class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||
class ArtResizerFileSizeTest(CleanupModulesMixin, _common.TestCase, TestHelper):
|
||||
"""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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,29 @@
|
|||
import os
|
||||
import unittest
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
from beets import config, ui
|
||||
from beets.library import Library
|
||||
from beets.test.helper import TestHelper
|
||||
|
||||
|
||||
class ConfigCommandTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.lib = Library(":memory:")
|
||||
self.temp_dir = mkdtemp()
|
||||
self.setup_beets()
|
||||
for k in ("VISUAL", "EDITOR"):
|
||||
if k in os.environ:
|
||||
del os.environ[k]
|
||||
|
||||
os.environ["BEETSDIR"] = self.temp_dir
|
||||
self.config_path = os.path.join(self.temp_dir, "config.yaml")
|
||||
temp_dir = self.temp_dir.decode()
|
||||
|
||||
self.config_path = os.path.join(temp_dir, "config.yaml")
|
||||
with open(self.config_path, "w") as file:
|
||||
file.write("library: lib\n")
|
||||
file.write("option: value\n")
|
||||
file.write("password: password_value")
|
||||
|
||||
self.cli_config_path = os.path.join(self.temp_dir, "cli_config.yaml")
|
||||
self.cli_config_path = os.path.join(temp_dir, "cli_config.yaml")
|
||||
with open(self.cli_config_path, "w") as file:
|
||||
file.write("option: cli overwrite")
|
||||
|
||||
|
|
@ -35,7 +32,7 @@ class ConfigCommandTest(unittest.TestCase, TestHelper):
|
|||
config._materialized = False
|
||||
|
||||
def tearDown(self):
|
||||
rmtree(self.temp_dir)
|
||||
self.teardown_beets()
|
||||
|
||||
def _run_with_yaml_output(self, *args):
|
||||
output = self.run_with_output(*args)
|
||||
|
|
|
|||
|
|
@ -1763,7 +1763,7 @@ class ImportPretendTest(_common.TestCase, ImportHelper):
|
|||
self.matcher = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.io = _common.DummyIO()
|
||||
self.setup_beets()
|
||||
self.__create_import_dir()
|
||||
self.__create_empty_import_dir()
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from beets.test.helper import (
|
|||
AutotagStub,
|
||||
ImportHelper,
|
||||
TerminalImportSessionSetup,
|
||||
TestHelper,
|
||||
)
|
||||
from beets.util import bytestring_path, displayable_path, syspath
|
||||
from beets.util.id_extractors import (
|
||||
|
|
@ -46,7 +47,7 @@ from beets.util.id_extractors import (
|
|||
)
|
||||
|
||||
|
||||
class TestHelper(helper.TestHelper):
|
||||
class PluginLoaderTestCase(unittest.TestCase, TestHelper):
|
||||
def setup_plugin_loader(self):
|
||||
# FIXME the mocking code is horrific, but this is the lowest and
|
||||
# earliest level of the plugin mechanism we can hook into.
|
||||
|
|
@ -68,8 +69,6 @@ class TestHelper(helper.TestHelper):
|
|||
def register_plugin(self, plugin_class):
|
||||
self._plugin_classes.add(plugin_class)
|
||||
|
||||
|
||||
class ItemTypesTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
|
||||
|
|
@ -77,6 +76,8 @@ class ItemTypesTest(unittest.TestCase, TestHelper):
|
|||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
|
||||
class ItemTypesTest(PluginLoaderTestCase):
|
||||
def test_flex_field_type(self):
|
||||
class RatingPlugin(plugins.BeetsPlugin):
|
||||
item_types = {"rating": types.Float()}
|
||||
|
|
@ -102,10 +103,9 @@ class ItemTypesTest(unittest.TestCase, TestHelper):
|
|||
self.assertNotIn("aaa", out)
|
||||
|
||||
|
||||
class ItemWriteTest(unittest.TestCase, TestHelper):
|
||||
class ItemWriteTest(PluginLoaderTestCase):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
self.setup_beets()
|
||||
super().setUp()
|
||||
|
||||
class EventListenerPlugin(plugins.BeetsPlugin):
|
||||
pass
|
||||
|
|
@ -113,10 +113,6 @@ class ItemWriteTest(unittest.TestCase, TestHelper):
|
|||
self.event_listener_plugin = EventListenerPlugin()
|
||||
self.register_plugin(EventListenerPlugin)
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
def test_change_tags(self):
|
||||
def on_write(item=None, path=None, tags=None):
|
||||
if tags["artist"] == "XXX":
|
||||
|
|
@ -134,15 +130,7 @@ class ItemWriteTest(unittest.TestCase, TestHelper):
|
|||
self.event_listener_plugin.register_listener(event, func)
|
||||
|
||||
|
||||
class ItemTypeConflictTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
self.setup_beets()
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
class ItemTypeConflictTest(PluginLoaderTestCase):
|
||||
def test_mismatch(self):
|
||||
class EventListenerPlugin(plugins.BeetsPlugin):
|
||||
item_types = {"duplicate": types.INTEGER}
|
||||
|
|
@ -170,17 +158,12 @@ class ItemTypeConflictTest(unittest.TestCase, TestHelper):
|
|||
self.assertIsNotNone(plugins.types(Item))
|
||||
|
||||
|
||||
class EventsTest(unittest.TestCase, ImportHelper, TestHelper):
|
||||
class EventsTest(ImportHelper, PluginLoaderTestCase):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
self.setup_beets()
|
||||
super().setUp()
|
||||
self.__create_import_dir(2)
|
||||
config["import"]["pretend"] = True
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
def __copy_file(self, dest_path, metadata):
|
||||
# Copy files
|
||||
resource_path = os.path.join(RSRC, b"full.mp3")
|
||||
|
|
@ -301,14 +284,7 @@ class HelpersTest(unittest.TestCase):
|
|||
)
|
||||
|
||||
|
||||
class ListenersTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
|
||||
class ListenersTest(PluginLoaderTestCase):
|
||||
def test_register(self):
|
||||
class DummyPlugin(plugins.BeetsPlugin):
|
||||
def __init__(self):
|
||||
|
|
@ -428,11 +404,10 @@ class ListenersTest(unittest.TestCase, TestHelper):
|
|||
|
||||
|
||||
class PromptChoicesTest(
|
||||
TerminalImportSessionSetup, unittest.TestCase, ImportHelper, TestHelper
|
||||
TerminalImportSessionSetup, ImportHelper, PluginLoaderTestCase
|
||||
):
|
||||
def setUp(self):
|
||||
self.setup_plugin_loader()
|
||||
self.setup_beets()
|
||||
super().setUp()
|
||||
self._create_import_dir(3)
|
||||
self._setup_import_session()
|
||||
self.matcher = AutotagStub().install()
|
||||
|
|
@ -443,9 +418,8 @@ class PromptChoicesTest(
|
|||
self.mock_input_options = self.input_options_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.input_options_patcher.stop()
|
||||
self.teardown_plugin_loader()
|
||||
self.teardown_beets()
|
||||
self.matcher.restore()
|
||||
|
||||
def test_plugin_choices_in_ui_input_options_album(self):
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class FieldsTest(_common.LibTestCase):
|
|||
self.io.install()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.io.restore()
|
||||
|
||||
def remove_keys(self, l, text):
|
||||
|
|
|
|||
Loading…
Reference in a new issue