From e4461e54c50cb53990fefbecbcfa593d016e8e90 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 1 Apr 2024 16:40:10 +0200 Subject: [PATCH] Move to pytest fixtures instead of unittest.TestCase --- test/cli_test.py | 66 +++++++++++++++++++++++---------------------- test/helper.py | 70 +++++++++--------------------------------------- 2 files changed, 47 insertions(+), 89 deletions(-) diff --git a/test/cli_test.py b/test/cli_test.py index 75859d5..1344787 100644 --- a/test/cli_test.py +++ b/test/cli_test.py @@ -1,6 +1,7 @@ import os import os.path import shutil +from pathlib import Path import pytest from beets import util @@ -24,13 +25,13 @@ ) -class DocTest(TestHelper): +class TestDoc(TestHelper): """Test alternatives in a larger-scale scenario with transcoding and multiple changes to the library. """ - def test_external(self): - external_dir = os.path.join(self.mkdtemp(), "myplayer") + def test_external(self, tmp_path: Path): + external_dir = str(tmp_path / "myplayer") self.config["convert"]["formats"] = { "aac": { "command": "bash -c \"cp '$source' '$dest';" @@ -109,12 +110,12 @@ def test_external(self): assert_file_tag(external_beet, b"ISAAC") -class SymlinkViewTest(TestHelper): +class TestSymlinkView(TestHelper): """Test alternatives with the ``link`` format producing symbolic links.""" - def setUp(self): - super().setUp() - self.set_paths_config({"default": "$artist/$album/$title"}) + @pytest.fixture(autouse=True) + def _symlink_view(self): + self.lib.path_formats = (("default", "$artist/$album/$title"),) self.config["alternatives"] = { "by-year": { "paths": {"default": "$year/$album/$title"}, @@ -233,17 +234,16 @@ def test_valid_options(self): self.runcli("alt", "update", "by-year") -class ExternalCopyTest(TestHelper): +class TestExternalCopy(TestHelper): """Test alternatives with empty ``format `` option, i.e. only copying without transcoding. """ - def setUp(self): - super().setUp() - external_dir = self.mkdtemp() + @pytest.fixture(autouse=True) + def _external_copy(self, tmp_path: Path, _setup: None): self.config["alternatives"] = { "myexternal": { - "directory": external_dir, + "directory": str(tmp_path), "query": "myexternal:true", } } @@ -375,7 +375,7 @@ def test_unkown_collection(self): self.runcli("alt", "update", "unkown") assert str(e.value) == "Alternative collection 'unkown' not found." - def test_embed_art(self): + def test_embed_art(self, tmp_path: Path): """Test that artwork is embedded and updated to match the source file. There used to be a bug that meant that albumart was only embedded @@ -409,8 +409,7 @@ def touch_art(item, image_path): # Make a copy of the artwork, so that changing mtime/content won't # affect the repository. - image_dir = bytestring_path(self.mkdtemp()) - image_path = os.path.join(image_dir, b"image") + image_path = bytes(tmp_path / "image") shutil.copy(self.IMAGE_FIXTURE1, check_type(syspath(image_path), bytes)) touch_art(item, image_path) @@ -432,9 +431,13 @@ def touch_art(item, image_path): item = album.items().get() assert_has_embedded_artwork(self.get_path(item), self.IMAGE_FIXTURE2) - def test_update_all(self): - dir_a = self.mkdtemp() - dir_b = self.mkdtemp() + def test_update_all(self, tmp_path: Path): + dir_a = tmp_path / "a" + dir_a.mkdir() + dir_a = str(dir_a) + dir_b = tmp_path / "b" + dir_b.mkdir() + dir_b = str(dir_b) self.config["alternatives"].get().clear() # type: ignore self.config["alternatives"] = { "a": { @@ -468,14 +471,14 @@ def test_update_all(self): assert self.runcli("alt", "update", "--all") == "" -class ExternalConvertTest(TestHelper): +class TestExternalConvert(TestHelper): """Test alternatives with non-empty ``format`` option, i.e. transcoding some of the files. """ - def setUp(self): - super().setUp() - external_dir = self.mkdtemp() + @pytest.fixture(autouse=True) + def _external_convert(self, tmp_path: Path, _setup: None): + external_dir = str(tmp_path) self.config["convert"]["formats"] = { "ogg": "bash -c \"cp '$source' '$dest';" + "printf ISOGG >> '$dest'\"" } @@ -555,15 +558,15 @@ def test_no_move_on_extension_change(self): assert_file_tag(converted_path, b"ISMP3") -class ExternalConvertWorkerTest(TestHelper): +class TestExternalConvertWorker(TestHelper): """Test alternatives with non-empty ``format`` option, i.e. transcoding some of the files. In contrast to the previous test, these test do use the parallelizing ``beetsplug.alternatives.Worker``. """ - def setUp(self): - super().setUp(mock_worker=False) - external_dir = self.mkdtemp() + def test_convert_multiple(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.undo() + external_dir = str(tmp_path) self.config["convert"]["formats"] = { "ogg": "bash -c \"cp '{source}' '$dest'\"".format( # The convert plugin will encode this again using arg_encoding @@ -578,7 +581,6 @@ def setUp(self): } } - def test_convert_multiple(self): items = [ self.add_track( title=f"track {i}", @@ -594,14 +596,14 @@ def test_convert_multiple(self): assert_media_file_fields(converted_path, type="ogg", title=item.title) -class ExternalRemovableTest(TestHelper): +class TestExternalRemovable(TestHelper): """Test whether alternatives properly detects ``removable`` collections and performs the expected user queries before doing anything. """ - def setUp(self): - super().setUp() - external_dir = os.path.join(self.mkdtemp(), "\u00e9xt") + @pytest.fixture(autouse=True) + def _external_removable(self, tmp_path: Path, _setup: None): + external_dir = str(tmp_path / "\u00e9xt") self.config["alternatives"] = { "myexternal": { "directory": external_dir, @@ -648,7 +650,7 @@ def test_not_removable(self): assert "alt.myexternal" in item -class CompletionTest(TestHelper): +class TestCompletion(TestHelper): """Test invocation of ``beet completion`` with this plugin. Only ensures that command does not fail. diff --git a/test/helper.py b/test/helper.py index 11fcaba..d97783d 100644 --- a/test/helper.py +++ b/test/helper.py @@ -1,17 +1,15 @@ import os -import shutil import sys -import tempfile from concurrent import futures from contextlib import contextmanager from io import StringIO +from pathlib import Path from typing import Optional -from unittest import TestCase -from unittest.mock import patch from zlib import crc32 import beets import beets.library +import pytest from beets import logging, plugins, ui, util from beets.library import Item from beets.util import MoveOperation, bytestring_path, displayable_path, syspath @@ -20,7 +18,10 @@ import beetsplug.alternatives as alternatives import beetsplug.convert as convert -logging.getLogger("beets").propagate = True +beetsLogger = logging.getLogger("beets") +beetsLogger.propagate = True +for h in beetsLogger.handlers: + beetsLogger.removeHandler(h) @contextmanager @@ -139,41 +140,12 @@ def assert_media_file_fields(path, **kwargs): assert actual == v, f"MediaFile has tag {k}='{actual}' " f"instead of '{v}'" -class TestHelper(TestCase): - def setUp(self, mock_worker=True): - """Setup required for running test. Must be called before - running any tests. +class TestHelper: + @pytest.fixture(autouse=True) + def _setup(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr("beetsplug.alternatives.Worker", MockedWorker) - If ``mock_worker`` is ``True`` the simple non-threaded - ``MockedWorker`` is used to run file conversion commands. In - particular, in contrast to the actual conversion routine from the - ``convert`` plugin, it will not attempt to write tags to the output - files. Thus, the 'converted' files need not be valid audio files. - """ - if mock_worker: - patcher = patch("beetsplug.alternatives.Worker", new=MockedWorker) - patcher.start() - self.addCleanup(patcher.stop) - - self._tempdirs = [] plugins._classes = {alternatives.AlternativesPlugin, convert.ConvertPlugin} - self.setup_beets() - - def tearDown(self): - self.unload_plugins() - for tempdir in self._tempdirs: - shutil.rmtree(syspath(tempdir)) - - def mkdtemp(self): - # This return a str path, i.e. Unicode on Python 3. We need this in - # order to put paths into the configuration. - path = tempfile.mkdtemp() - self._tempdirs.append(path) - return path - - def setup_beets(self): - self.addCleanup(self.teardown_beets) - os.environ["BEETSDIR"] = self.mkdtemp() self.config = beets.config self.config.clear() @@ -185,7 +157,8 @@ def setup_beets(self): self.config["threaded"] = False self.config["import"]["copy"] = False - libdir = self.mkdtemp() + libdir = str(tmp_path / "beets_lib") + os.environ["BEETSDIR"] = libdir self.config["directory"] = libdir self.libdir = bytestring_path(libdir) @@ -199,18 +172,7 @@ def setup_beets(self): self.IMAGE_FIXTURE1 = os.path.join(self.fixture_dir, b"image.png") self.IMAGE_FIXTURE2 = os.path.join(self.fixture_dir, b"image_black.png") - - def teardown_beets(self): - del self.lib._connections - if "BEETSDIR" in os.environ: - del os.environ["BEETSDIR"] - self.config.clear() - beets.config.read(user=False, defaults=True) - - def set_paths_config(self, conf): - self.lib.path_formats = conf.items() - - def unload_plugins(self): + yield for plugin in plugins._classes: plugin.listeners = None plugins._classes = set() @@ -294,12 +256,6 @@ def submit(self, *args, **kwargs): fut = futures.Future() res = self._fn(*args, **kwargs) fut.set_result(res) - # try: - # res = fn(*args, **kwargs) - # except Exception as e: - # fut.set_exception(e) - # else: - # fut.set_result(res) self._tasks.add(fut) return fut