This commit is contained in:
Adrian Sampson 2026-02-03 03:28:48 +10:00 committed by GitHub
commit 8c42600354
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 279 additions and 334 deletions

View file

@ -14,10 +14,13 @@
"""Some common functionality for beets' test cases."""
from __future__ import annotations
import os
import sys
import unittest
from contextlib import contextmanager
from typing import TYPE_CHECKING
import beets
import beets.library
@ -28,6 +31,9 @@ from beets import importer, logging, util
from beets.ui import commands
from beets.util import syspath
if TYPE_CHECKING:
import pytest
beetsplug.__path__ = [
os.path.abspath(
os.path.join(
@ -118,85 +124,55 @@ def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
# Mock I/O.
class InputError(Exception):
def __init__(self, output=None):
self.output = output
def __str__(self):
msg = "Attempt to read with no input provided."
if self.output is not None:
msg += f" Output: {self.output!r}"
return msg
class DummyOut:
encoding = "utf-8"
def __init__(self):
self.buf = []
def write(self, s):
self.buf.append(s)
def get(self):
return "".join(self.buf)
def flush(self):
self.clear()
def clear(self):
self.buf = []
class InputError(IOError):
def __str__(self) -> str:
return "Attempt to read with no input provided."
class DummyIn:
encoding = "utf-8"
def __init__(self, out=None):
self.buf = []
self.reads = 0
self.out = out
def __init__(self) -> None:
self.buf: list[str] = []
def add(self, s):
def add(self, s: str) -> None:
self.buf.append(f"{s}\n")
def close(self):
def close(self) -> None:
pass
def readline(self):
def readline(self) -> str:
if not self.buf:
if self.out:
raise InputError(self.out.get())
else:
raise InputError()
self.reads += 1
raise InputError
return self.buf.pop(0)
class DummyIO:
"""Mocks input and output streams for testing UI code."""
"""Test helper that manages standard input and output."""
def __init__(self):
self.stdout = DummyOut()
self.stdin = DummyIn(self.stdout)
def __init__(
self,
monkeypatch: pytest.MonkeyPatch,
capteesys: pytest.CaptureFixture[str],
) -> None:
self._capteesys = capteesys
self.stdin = DummyIn()
def addinput(self, s):
self.stdin.add(s)
monkeypatch.setattr("sys.stdin", self.stdin)
def getoutput(self):
res = self.stdout.get()
self.stdout.clear()
return res
def addinput(self, text: str) -> None:
"""Simulate user typing into stdin."""
self.stdin.add(text)
def readcount(self):
return self.stdin.reads
def getoutput(self) -> str:
"""Get the standard output captured so far.
def install(self):
sys.stdin = self.stdin
sys.stdout = self.stdout
def restore(self):
sys.stdin = sys.__stdin__
sys.stdout = sys.__stdout__
Note: it clears the internal buffer, so subsequent calls will only
return *new* output.
"""
# Using capteesys allows you to see output in the console if the test fails
return self._capteesys.readouterr().out
# Utility.

View file

@ -15,9 +15,6 @@
"""This module includes various helpers that provide fixtures, capture
information or mock the environment.
- The `control_stdin` and `capture_stdout` context managers allow one to
interact with the user interface.
- `has_program` checks the presence of a command on the system.
- The `ImportSessionFixture` allows one to run importer code while
@ -38,12 +35,12 @@ from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from io import StringIO
from pathlib import Path
from tempfile import gettempdir, mkdtemp, mkstemp
from typing import Any, ClassVar
from unittest.mock import patch
import pytest
import responses
from mediafile import Image, MediaFile
@ -83,41 +80,6 @@ def capture_log(logger="beets"):
log.removeHandler(capture)
@contextmanager
def control_stdin(input=None):
"""Sends ``input`` to stdin.
>>> with control_stdin('yes'):
... input()
'yes'
"""
org = sys.stdin
sys.stdin = StringIO(input)
try:
yield sys.stdin
finally:
sys.stdin = org
@contextmanager
def capture_stdout():
"""Save stdout in a StringIO.
>>> with capture_stdout() as output:
... print('spam')
...
>>> output.getvalue()
'spam'
"""
org = sys.stdout
sys.stdout = capture = StringIO()
try:
yield sys.stdout
finally:
sys.stdout = org
print(capture.getvalue())
def has_program(cmd, args=["--version"]):
"""Returns `True` if `cmd` can be executed."""
full_cmd = [cmd, *args]
@ -163,21 +125,31 @@ NEEDS_REFLINK = unittest.skipUnless(
)
class IOMixin:
@cached_property
def io(self) -> _common.DummyIO:
return _common.DummyIO()
def setUp(self):
super().setUp()
self.io.install()
def tearDown(self):
super().tearDown()
self.io.restore()
class RunMixin:
def run_command(self, *args, **kwargs):
"""Run a beets command with an arbitrary amount of arguments. The
Library` defaults to `self.lib`, but can be overridden with
the keyword argument `lib`.
"""
sys.argv = ["beet"] # avoid leakage from test suite args
lib = None
if hasattr(self, "lib"):
lib = self.lib
lib = kwargs.get("lib", lib)
beets.ui._raw_main(list(args), lib)
class TestHelper(ConfigMixin):
@pytest.mark.usefixtures("io")
class IOMixin(RunMixin):
io: _common.DummyIO
def run_with_output(self, *args):
self.io.getoutput()
self.run_command(*args)
return self.io.getoutput()
class TestHelper(RunMixin, ConfigMixin):
"""Helper mixin for high-level cli and plugin tests.
This mixin provides methods to isolate beets' global state provide
@ -392,25 +364,6 @@ class TestHelper(ConfigMixin):
return path
# Running beets commands
def run_command(self, *args, **kwargs):
"""Run a beets command with an arbitrary amount of arguments. The
Library` defaults to `self.lib`, but can be overridden with
the keyword argument `lib`.
"""
sys.argv = ["beet"] # avoid leakage from test suite args
lib = None
if hasattr(self, "lib"):
lib = self.lib
lib = kwargs.get("lib", lib)
beets.ui._raw_main(list(args), lib)
def run_with_output(self, *args):
with capture_stdout() as out:
self.run_command(*args)
return out.getvalue()
# Safe file operations
def create_temp_dir(self, **kwargs) -> str:
@ -758,10 +711,7 @@ class TerminalImportSessionFixture(TerminalImportSession):
class TerminalImportMixin(IOMixin, ImportHelper):
"""Provides_a terminal importer for the import session."""
io: _common.DummyIO
def _get_import_session(self, import_dir: bytes) -> importer.ImportSession:
self.io.install()
return TerminalImportSessionFixture(
self.lib,
loghandler=None,

View file

@ -5,6 +5,7 @@ import pytest
from beets.autotag.distance import Distance
from beets.dbcore.query import Query
from beets.test._common import DummyIO
from beets.test.helper import ConfigMixin
from beets.util import cached_classproperty
@ -60,3 +61,24 @@ def clear_cached_classproperty():
def config():
"""Provide a fresh beets configuration for a module, when requested."""
return ConfigMixin().config
@pytest.fixture
def io(
request: pytest.FixtureRequest,
monkeypatch: pytest.MonkeyPatch,
capteesys: pytest.CaptureFixture[str],
) -> DummyIO:
"""Fixture for tests that need controllable stdin and captured stdout.
This fixture builds a per-test ``DummyIO`` helper and exposes it to the
test. When used on a test class, it attaches the helper as ``self.io``
attribute to make it available to all test methods, including
``unittest.TestCase``-based ones.
"""
io = DummyIO(monkeypatch, capteesys)
if request.instance:
request.instance.io = io
return io

View file

@ -4,10 +4,10 @@
"""Tests for the 'bareasc' plugin."""
from beets import logging
from beets.test.helper import PluginTestCase, capture_stdout
from beets.test.helper import IOMixin, PluginTestCase
class BareascPluginTest(PluginTestCase):
class BareascPluginTest(IOMixin, PluginTestCase):
"""Test bare ASCII query matching."""
plugin = "bareasc"
@ -65,16 +65,12 @@ class BareascPluginTest(PluginTestCase):
def test_bareasc_list_output(self):
"""Bare-ASCII version of list command - check output."""
with capture_stdout() as output:
self.run_command("bareasc", "with accents")
self.run_command("bareasc", "with accents")
assert "Antonin Dvorak" in output.getvalue()
assert "Antonin Dvorak" in self.io.getoutput()
def test_bareasc_format_output(self):
"""Bare-ASCII version of list -f command - check output."""
with capture_stdout() as output:
self.run_command(
"bareasc", "with accents", "-f", "$artist:: $title"
)
self.run_command("bareasc", "with accents", "-f", "$artist:: $title")
assert "Antonin Dvorak:: with accents\n" == output.getvalue()
assert "Antonin Dvorak:: with accents\n" == self.io.getoutput()

View file

@ -32,7 +32,7 @@ import yaml
from beets.test.helper import PluginTestCase
from beets.util import bluelet
bpd = pytest.importorskip("beetsplug.bpd")
bpd = pytest.importorskip("beetsplug.bpd", exc_type=ImportError)
class CommandParseTest(unittest.TestCase):

View file

@ -29,9 +29,9 @@ from beets.test import _common
from beets.test.helper import (
AsIsImporterMixin,
ImportHelper,
IOMixin,
PluginTestCase,
capture_log,
control_stdin,
)
from beetsplug import convert
@ -66,7 +66,7 @@ class ConvertMixin:
return path.read_bytes().endswith(tag.encode("utf-8"))
class ConvertTestCase(ConvertMixin, PluginTestCase):
class ConvertTestCase(IOMixin, ConvertMixin, PluginTestCase):
db_on_disk = True
plugin = "convert"
@ -157,8 +157,8 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
}
def test_convert(self):
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert self.file_endswith(self.converted_mp3, "mp3")
def test_convert_with_auto_confirmation(self):
@ -166,22 +166,22 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
assert self.file_endswith(self.converted_mp3, "mp3")
def test_reject_confirmation(self):
with control_stdin("n"):
self.run_convert()
self.io.addinput("n")
self.run_convert()
assert not self.converted_mp3.exists()
def test_convert_keep_new(self):
assert os.path.splitext(self.item.path)[1] == b".ogg"
with control_stdin("y"):
self.run_convert("--keep-new")
self.io.addinput("y")
self.run_convert("--keep-new")
self.item.load()
assert os.path.splitext(self.item.path)[1] == b".mp3"
def test_format_option(self):
with control_stdin("y"):
self.run_convert("--format", "opus")
self.io.addinput("y")
self.run_convert("--format", "opus")
assert self.file_endswith(self.convert_dest / "converted.ops", "opus")
def test_embed_album_art(self):
@ -192,8 +192,8 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
with open(os.path.join(image_path), "rb") as f:
image_data = f.read()
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
mediafile = MediaFile(self.converted_mp3)
assert mediafile.images[0].data == image_data
@ -215,26 +215,26 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
def test_no_transcode_when_maxbr_set_high_and_different_formats(self):
self.config["convert"]["max_bitrate"] = 5000
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert self.file_endswith(self.converted_mp3, "mp3")
def test_transcode_when_maxbr_set_low_and_different_formats(self):
self.config["convert"]["max_bitrate"] = 5
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert self.file_endswith(self.converted_mp3, "mp3")
def test_transcode_when_maxbr_set_to_none_and_different_formats(self):
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert self.file_endswith(self.converted_mp3, "mp3")
def test_no_transcode_when_maxbr_set_high_and_same_formats(self):
self.config["convert"]["max_bitrate"] = 5000
self.config["convert"]["format"] = "ogg"
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert not self.file_endswith(
self.convert_dest / "converted.ogg", "ogg"
)
@ -243,8 +243,8 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
self.config["convert"]["max_bitrate"] = 5000
self.config["convert"]["format"] = "ogg"
with control_stdin("y"):
self.run_convert("--force")
self.io.addinput("y")
self.run_convert("--force")
converted = self.convert_dest / "converted.ogg"
assert self.file_endswith(converted, "ogg")
@ -252,21 +252,21 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
def test_transcode_when_maxbr_set_low_and_same_formats(self):
self.config["convert"]["max_bitrate"] = 5
self.config["convert"]["format"] = "ogg"
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert self.file_endswith(self.convert_dest / "converted.ogg", "ogg")
def test_transcode_when_maxbr_set_to_none_and_same_formats(self):
self.config["convert"]["format"] = "ogg"
with control_stdin("y"):
self.run_convert()
self.io.addinput("y")
self.run_convert()
assert not self.file_endswith(
self.convert_dest / "converted.ogg", "ogg"
)
def test_playlist(self):
with control_stdin("y"):
self.run_convert("--playlist", "playlist.m3u8")
self.io.addinput("y")
self.run_convert("--playlist", "playlist.m3u8")
assert (self.convert_dest / "playlist.m3u8").exists()
def test_playlist_pretend(self):
@ -282,8 +282,8 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
[item] = self.add_item_fixtures(ext="ogg")
with control_stdin("y"):
self.run_convert_path(item, "--format", "opus", "--force")
self.io.addinput("y")
self.run_convert_path(item, "--format", "opus", "--force")
converted = self.convert_dest / "converted.ops"
assert self.file_endswith(converted, "opus")
@ -309,23 +309,23 @@ class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand):
def test_transcode_from_lossless(self):
[item] = self.add_item_fixtures(ext="flac")
with control_stdin("y"):
self.run_convert_path(item)
self.io.addinput("y")
self.run_convert_path(item)
converted = self.convert_dest / "converted.mp3"
assert self.file_endswith(converted, "mp3")
def test_transcode_from_lossy(self):
self.config["convert"]["never_convert_lossy_files"] = False
[item] = self.add_item_fixtures(ext="ogg")
with control_stdin("y"):
self.run_convert_path(item)
self.io.addinput("y")
self.run_convert_path(item)
converted = self.convert_dest / "converted.mp3"
assert self.file_endswith(converted, "mp3")
def test_transcode_from_lossy_prevented(self):
[item] = self.add_item_fixtures(ext="ogg")
with control_stdin("y"):
self.run_convert_path(item)
self.io.addinput("y")
self.run_convert_path(item)
converted = self.convert_dest / "converted.ogg"
assert not self.file_endswith(converted, "mp3")
@ -336,8 +336,8 @@ class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand):
}
[item] = self.add_item_fixtures(ext="ogg")
with control_stdin("y"):
self.run_convert_path(item, "--format", "opus", "--force")
self.io.addinput("y")
self.run_convert_path(item, "--format", "opus", "--force")
converted = self.convert_dest / "converted.ops"
assert self.file_endswith(converted, "opus")

View file

@ -17,15 +17,16 @@ from typing import ClassVar
from unittest.mock import patch
from beets.dbcore.query import TrueQuery
from beets.importer import Action
from beets.library import Item
from beets.test import _common
from beets.test.helper import (
AutotagImportTestCase,
AutotagStub,
BeetsTestCase,
IOMixin,
PluginMixin,
TerminalImportMixin,
control_stdin,
)
@ -103,24 +104,26 @@ class EditMixin(PluginMixin):
"""
m = ModifyFileMocker(**modify_file_args)
with patch("beetsplug.edit.edit", side_effect=m.action):
with control_stdin("\n".join(stdin)):
self.importer.run()
for char in stdin:
self.importer.add_choice(char)
self.importer.run()
def run_mocked_command(self, modify_file_args={}, stdin=[], args=[]):
"""Run the edit command, with mocked stdin and yaml writing, and
passing `args` to `run_command`."""
m = ModifyFileMocker(**modify_file_args)
with patch("beetsplug.edit.edit", side_effect=m.action):
with control_stdin("\n".join(stdin)):
self.run_command("edit", *args)
for char in stdin:
self.io.addinput(char)
self.run_command("edit", *args)
@_common.slow_test()
@patch("beets.library.Item.write")
class EditCommandTest(EditMixin, BeetsTestCase):
class EditCommandTest(IOMixin, EditMixin, BeetsTestCase):
"""Black box tests for `beetsplug.edit`. Command line interaction is
simulated using `test.helper.control_stdin()`, and yaml editing via an
external editor is simulated using `ModifyFileMocker`.
simulated using mocked stdin, and yaml editing via an external editor is
simulated using `ModifyFileMocker`.
"""
ALBUM_COUNT = 1
@ -412,7 +415,7 @@ class EditDuringImporterNonSingletonTest(EditDuringImporterTestCase):
self.run_mocked_interpreter(
{},
# 1, Apply changes.
["1", "a"],
["1", Action.APPLY],
)
# Retag and edit track titles. On retag, the importer will reset items

View file

@ -19,10 +19,10 @@ import re # used to test csv format
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
class ExportPluginTest(PluginTestCase):
class ExportPluginTest(IOMixin, PluginTestCase):
plugin = "export"
def setUp(self):

View file

@ -18,10 +18,10 @@ import os
import sys
from beets import util
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
class FetchartCliTest(PluginTestCase):
class FetchartCliTest(IOMixin, PluginTestCase):
plugin = "fetchart"
def setUp(self):

View file

@ -454,7 +454,9 @@ def test_custom_words(
assert ftintitle.contains_feat(given, custom_words) is expected
def test_album_template_value():
def test_album_template_value(config):
config["ftintitle"]["custom_words"] = []
album = Album()
album["albumartist"] = "Foo ft. Bar"
assert ftintitle._album_artist_no_feat(album) == "Foo"

View file

@ -19,7 +19,7 @@ import os
import time
from beets import importer, plugins
from beets.test.helper import AutotagImportTestCase, PluginMixin, control_stdin
from beets.test.helper import AutotagImportTestCase, IOMixin, PluginMixin
from beets.util import syspath
from beetsplug.importsource import ImportSourcePlugin
@ -34,7 +34,7 @@ def preserve_plugin_listeners():
ImportSourcePlugin.listeners = _listeners
class ImportSourceTest(PluginMixin, AutotagImportTestCase):
class ImportSourceTest(IOMixin, PluginMixin, AutotagImportTestCase):
plugin = "importsource"
preload_plugin = False
@ -50,31 +50,29 @@ class ImportSourceTest(PluginMixin, AutotagImportTestCase):
self.all_items = self.lib.albums().get().items()
self.item_to_remove = self.all_items[0]
def interact(self, stdin_input: str):
with control_stdin(stdin_input):
self.run_command(
"remove",
f"path:{syspath(self.item_to_remove.path)}",
)
def interact(self, stdin: list[str]):
for char in stdin:
self.io.addinput(char)
self.run_command("remove", f"path:{syspath(self.item_to_remove.path)}")
def test_do_nothing(self):
self.interact("N")
self.interact(["N"])
assert os.path.exists(self.item_to_remove.source_path)
def test_remove_single(self):
self.interact("y\nD")
self.interact(["y", "D"])
assert not os.path.exists(self.item_to_remove.source_path)
def test_remove_all_from_single(self):
self.interact("y\nR\ny")
self.interact(["y", "R", "y"])
for item in self.all_items:
assert not os.path.exists(item.source_path)
def test_stop_suggesting(self):
self.interact("y\nS")
self.interact(["y", "S"])
for item in self.all_items:
assert os.path.exists(item.source_path)

View file

@ -15,11 +15,11 @@
from mediafile import MediaFile
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
from beets.util import displayable_path
class InfoTest(PluginTestCase):
class InfoTest(IOMixin, PluginTestCase):
plugin = "info"
def test_path(self):

View file

@ -19,11 +19,11 @@ from unittest.mock import Mock, patch
import pytest
from beets.test import _common
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
from beetsplug import lastgenre
class LastGenrePluginTest(PluginTestCase):
class LastGenrePluginTest(IOMixin, PluginTestCase):
plugin = "lastgenre"
def setUp(self):

View file

@ -13,10 +13,10 @@
"""Tests for the 'limit' plugin."""
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
class LimitPluginTest(PluginTestCase):
class LimitPluginTest(IOMixin, PluginTestCase):
"""Unit tests for LimitPlugin
Note: query prefix tests do not work correctly with `run_with_output`.

View file

@ -17,8 +17,6 @@ from beets.test.helper import (
AutotagImportTestCase,
PluginMixin,
TerminalImportMixin,
capture_stdout,
control_stdin,
)
@ -34,10 +32,10 @@ class MBSubmitPluginTest(
def test_print_tracks_output(self):
"""Test the output of the "print tracks" choice."""
with capture_stdout() as output:
with control_stdin("\n".join(["p", "s"])):
# Print tracks; Skip
self.importer.run()
self.io.addinput("p")
self.io.addinput("s")
# Print tracks; Skip
self.importer.run()
# Manually build the string for comparing the output.
tracklist = (
@ -45,17 +43,19 @@ class MBSubmitPluginTest(
"01. Tag Track 1 - Tag Artist (0:01)\n"
"02. Tag Track 2 - Tag Artist (0:01)"
)
assert tracklist in output.getvalue()
assert tracklist in self.io.getoutput()
def test_print_tracks_output_as_tracks(self):
"""Test the output of the "print tracks" choice, as singletons."""
with capture_stdout() as output:
with control_stdin("\n".join(["t", "s", "p", "s"])):
# as Tracks; Skip; Print tracks; Skip
self.importer.run()
self.io.addinput("t")
self.io.addinput("s")
self.io.addinput("p")
self.io.addinput("s")
# as Tracks; Skip; Print tracks; Skip
self.importer.run()
# Manually build the string for comparing the output.
tracklist = (
"Open files with Picard? 02. Tag Track 2 - Tag Artist (0:01)"
)
assert tracklist in output.getvalue()
assert tracklist in self.io.getoutput()

View file

@ -3,20 +3,23 @@ import uuid
import pytest
from beets.library import Album
from beets.test.helper import PluginMixin, TestHelper
from beets.test.helper import IOMixin, PluginMixin, TestHelper
@pytest.fixture
def helper():
def helper(request):
helper = TestHelper()
helper.setup_beets()
yield helper
request.instance.lib = helper.lib
yield
helper.teardown_beets()
class TestMissingAlbums(PluginMixin):
@pytest.mark.usefixtures("helper")
class TestMissingAlbums(IOMixin, PluginMixin):
plugin = "missing"
album_in_lib = Album(
album="Album",
@ -47,15 +50,13 @@ class TestMissingAlbums(PluginMixin):
],
)
def test_missing_artist_albums(
self, requests_mock, helper, release_from_mb, expected_output
self, requests_mock, release_from_mb, expected_output
):
helper.lib.add(self.album_in_lib)
self.lib.add(self.album_in_lib)
requests_mock.get(
f"/ws/2/release-group?artist={self.album_in_lib.mb_albumartistid}",
json={"release-groups": [release_from_mb]},
)
with self.configure_plugin({}):
assert (
helper.run_with_output("missing", "--album") == expected_output
)
assert self.run_with_output("missing", "--album") == expected_output

View file

@ -21,14 +21,14 @@ from unittest.mock import ANY, patch
import pytest
from beets.test.helper import CleanupModulesMixin, PluginTestCase, control_stdin
from beets.test.helper import CleanupModulesMixin, IOMixin, PluginTestCase
from beets.ui import UserError
from beets.util import open_anything
from beetsplug.play import PlayPlugin
@patch("beetsplug.play.util.interactive_open")
class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
class PlayPluginTest(IOMixin, CleanupModulesMixin, PluginTestCase):
modules = (PlayPlugin.__module__,)
plugin = "play"
@ -127,8 +127,8 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
self.config["play"]["warning_threshold"] = 1
self.add_item(title="another NiceTitle")
with control_stdin("a"):
self.run_command("play", "nice")
self.io.addinput("a")
self.run_command("play", "nice")
open_mock.assert_not_called()
@ -138,12 +138,12 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase):
expected_playlist = f"{self.item.filepath}\n{self.other_item.filepath}"
with control_stdin("a"):
self.run_and_assert(
open_mock,
["-y", "NiceTitle"],
expected_playlist=expected_playlist,
)
self.io.addinput("a")
self.run_and_assert(
open_mock,
["-y", "NiceTitle"],
expected_playlist=expected_playlist,
)
def test_command_failed(self, open_mock):
open_mock.side_effect = OSError("some reason")

View file

@ -24,7 +24,7 @@ import pytest
from beets import config
from beets.dbcore.query import FixedFieldSort, MultipleSort, NullSort
from beets.library import Album, Item, parse_query_string
from beets.test.helper import BeetsTestCase, PluginTestCase
from beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase
from beets.ui import UserError
from beets.util import CHAR_REPLACE, syspath
from beetsplug.smartplaylist import SmartPlaylistPlugin
@ -458,7 +458,7 @@ class SmartPlaylistTest(BeetsTestCase):
assert content.count(b"/item2.mp3") == 1
class SmartPlaylistCLITest(PluginTestCase):
class SmartPlaylistCLITest(IOMixin, PluginTestCase):
plugin = "smartplaylist"
def setUp(self):

View file

@ -19,10 +19,10 @@ from datetime import datetime
import pytest
from confuse import ConfigValueError
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
class TypesPluginTest(PluginTestCase):
class TypesPluginTest(IOMixin, PluginTestCase):
plugin = "types"
def test_integer_modify_and_query(self):

View file

@ -3,12 +3,12 @@
from mediafile import MediaFile
from beets.library import Item
from beets.test.helper import PluginTestCase, control_stdin
from beets.test.helper import IOMixin, PluginTestCase
from beets.util import syspath
from beetsplug.zero import ZeroPlugin
class ZeroPluginTest(PluginTestCase):
class ZeroPluginTest(IOMixin, PluginTestCase):
plugin = "zero"
preload_plugin = False
@ -102,12 +102,10 @@ class ZeroPluginTest(PluginTestCase):
item.write()
item_id = item.id
with (
self.configure_plugin(
{"fields": ["comments"], "update_database": True, "auto": False}
),
control_stdin("y"),
with self.configure_plugin(
{"fields": ["comments"], "update_database": True, "auto": False}
):
self.io.addinput("y")
self.run_command("zero")
mf = MediaFile(syspath(item.path))
@ -125,16 +123,14 @@ class ZeroPluginTest(PluginTestCase):
item.write()
item_id = item.id
with (
self.configure_plugin(
{
"fields": ["comments"],
"update_database": False,
"auto": False,
}
),
control_stdin("y"),
with self.configure_plugin(
{
"fields": ["comments"],
"update_database": False,
"auto": False,
}
):
self.io.addinput("y")
self.run_command("zero")
mf = MediaFile(syspath(item.path))
@ -187,7 +183,8 @@ class ZeroPluginTest(PluginTestCase):
item_id = item.id
with self.configure_plugin({"fields": []}), control_stdin("y"):
with self.configure_plugin({"fields": []}):
self.io.addinput("y")
self.run_command("zero")
item = self.lib.get_item(item_id)
@ -203,12 +200,10 @@ class ZeroPluginTest(PluginTestCase):
item_id = item.id
with (
self.configure_plugin(
{"fields": ["year"], "keep_fields": ["comments"]}
),
control_stdin("y"),
with self.configure_plugin(
{"fields": ["year"], "keep_fields": ["comments"]}
):
self.io.addinput("y")
self.run_command("zero")
item = self.lib.get_item(item_id)
@ -303,12 +298,10 @@ class ZeroPluginTest(PluginTestCase):
)
item.write()
item_id = item.id
with (
self.configure_plugin(
{"fields": ["comments"], "update_database": True, "auto": False}
),
control_stdin("n"),
with self.configure_plugin(
{"fields": ["comments"], "update_database": True, "auto": False}
):
self.io.addinput("n")
self.run_command("zero")
mf = MediaFile(syspath(item.path))

View file

@ -20,7 +20,7 @@ from datetime import datetime
from beets.library import Item
from beets.test import _common
from beets.test.helper import PluginTestCase
from beets.test.helper import IOMixin, PluginTestCase
def _parsetime(s):
@ -31,7 +31,7 @@ def _is_windows():
return platform.system() == "Windows"
class MetaSyncTest(PluginTestCase):
class MetaSyncTest(IOMixin, PluginTestCase):
plugin = "metasync"
itunes_library_unix = os.path.join(_common.RSRC, b"itunes_library_unix.xml")
itunes_library_windows = os.path.join(

View file

@ -38,6 +38,7 @@ from beets.test import helper
from beets.test.helper import (
AutotagStub,
ImportHelper,
IOMixin,
PluginMixin,
PluginTestCase,
TerminalImportMixin,
@ -45,7 +46,7 @@ from beets.test.helper import (
from beets.util import PromptChoice, displayable_path, syspath
class TestPluginRegistration(PluginTestCase):
class TestPluginRegistration(IOMixin, PluginTestCase):
class RatingPlugin(plugins.BeetsPlugin):
item_types: ClassVar[dict[str, types.Type]] = {
"rating": types.Float(),
@ -429,8 +430,9 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase):
# DummyPlugin.foo() should be called once
with patch.object(DummyPlugin, "foo", autospec=True) as mock_foo:
with helper.control_stdin("\n".join(["f", "s"])):
self.importer.run()
self.io.addinput("f")
self.io.addinput("n")
self.importer.run()
assert mock_foo.call_count == 1
# input_options should be called twice, as foo() returns None
@ -471,8 +473,8 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase):
)
# DummyPlugin.foo() should be called once
with helper.control_stdin("f\n"):
self.importer.run()
self.io.addinput("f")
self.importer.run()
# input_options should be called once, as foo() returns SKIP
self.mock_input_options.assert_called_once_with(

View file

@ -49,7 +49,6 @@ class CompletionTest(IOMixin, TestPluginTestCase):
# Load completion script.
self.run_command("completion", lib=None)
completion_script = self.io.getoutput().encode("utf-8")
self.io.restore()
tester.stdin.writelines(completion_script.splitlines(True))
# Load test suite.

View file

@ -5,10 +5,10 @@ import pytest
import yaml
from beets import config, ui
from beets.test.helper import BeetsTestCase
from beets.test.helper import BeetsTestCase, IOMixin
class ConfigCommandTest(BeetsTestCase):
class ConfigCommandTest(IOMixin, BeetsTestCase):
def setUp(self):
super().setUp()
for k in ("VISUAL", "EDITOR"):

View file

@ -16,7 +16,7 @@ class FieldsTest(IOMixin, ItemInDBTestCase):
items = library.Item.all_keys()
albums = library.Album.all_keys()
output = self.io.stdout.get().split()
output = self.io.getoutput().split()
self.remove_keys(items, output)
self.remove_keys(albums, output)

View file

@ -1,9 +1,9 @@
from beets.test import _common
from beets.test.helper import BeetsTestCase, capture_stdout
from beets.test.helper import BeetsTestCase, IOMixin
from beets.ui.commands.list import list_items
class ListTest(BeetsTestCase):
class ListTest(IOMixin, BeetsTestCase):
def setUp(self):
super().setUp()
self.item = _common.item()
@ -12,13 +12,12 @@ class ListTest(BeetsTestCase):
self.lib.add_album([self.item])
def _run_list(self, query="", album=False, path=False, fmt=""):
with capture_stdout() as stdout:
list_items(self.lib, query, album, fmt)
return stdout
list_items(self.lib, query, album, fmt)
return self.io.getoutput()
def test_list_outputs_item(self):
stdout = self._run_list()
assert "the title" in stdout.getvalue()
assert "the title" in stdout
def test_list_unicode_query(self):
self.item.title = "na\xefve"
@ -26,44 +25,44 @@ class ListTest(BeetsTestCase):
self.lib._connection().commit()
stdout = self._run_list(["na\xefve"])
out = stdout.getvalue()
out = stdout
assert "na\xefve" in out
def test_list_item_path(self):
stdout = self._run_list(fmt="$path")
assert stdout.getvalue().strip() == "xxx/yyy"
assert stdout.strip() == "xxx/yyy"
def test_list_album_outputs_something(self):
stdout = self._run_list(album=True)
assert len(stdout.getvalue()) > 0
assert len(stdout) > 0
def test_list_album_path(self):
stdout = self._run_list(album=True, fmt="$path")
assert stdout.getvalue().strip() == "xxx"
assert stdout.strip() == "xxx"
def test_list_album_omits_title(self):
stdout = self._run_list(album=True)
assert "the title" not in stdout.getvalue()
assert "the title" not in stdout
def test_list_uses_track_artist(self):
stdout = self._run_list()
assert "the artist" in stdout.getvalue()
assert "the album artist" not in stdout.getvalue()
assert "the artist" in stdout
assert "the album artist" not in stdout
def test_list_album_uses_album_artist(self):
stdout = self._run_list(album=True)
assert "the artist" not in stdout.getvalue()
assert "the album artist" in stdout.getvalue()
assert "the artist" not in stdout
assert "the album artist" in stdout
def test_list_item_format_artist(self):
stdout = self._run_list(fmt="$artist")
assert "the artist" in stdout.getvalue()
assert "the artist" in stdout
def test_list_item_format_multiple(self):
stdout = self._run_list(fmt="$artist - $album - $year")
assert "the artist - the album - 0001" == stdout.getvalue().strip()
assert "the artist - the album - 0001" == stdout.strip()
def test_list_album_format(self):
stdout = self._run_list(album=True, fmt="$genre")
assert "the genre" in stdout.getvalue()
assert "the album" not in stdout.getvalue()
assert "the genre" in stdout
assert "the album" not in stdout

View file

@ -2,23 +2,24 @@ import unittest
from mediafile import MediaFile
from beets.test.helper import BeetsTestCase, control_stdin
from beets.test.helper import BeetsTestCase, IOMixin
from beets.ui.commands.modify import modify_parse_args
from beets.util import syspath
class ModifyTest(BeetsTestCase):
class ModifyTest(IOMixin, BeetsTestCase):
def setUp(self):
super().setUp()
self.album = self.add_album_fixture()
[self.item] = self.album.items()
def modify_inp(self, inp, *args):
with control_stdin(inp):
self.run_command("modify", *args)
def modify_inp(self, inp: list[str], *args):
for chat in inp:
self.io.addinput(chat)
self.run_command("modify", *args)
def modify(self, *args):
self.modify_inp("y", *args)
self.modify_inp(["y"], *args)
# Item tests
@ -30,14 +31,14 @@ class ModifyTest(BeetsTestCase):
def test_modify_item_abort(self):
item = self.lib.items().get()
title = item.title
self.modify_inp("n", "title=newTitle")
self.modify_inp(["n"], "title=newTitle")
item = self.lib.items().get()
assert item.title == title
def test_modify_item_no_change(self):
title = "Tracktitle"
item = self.add_item_fixture(title=title)
self.modify_inp("y", "title", f"title={title}")
self.modify_inp(["y"], "title", f"title={title}")
item = self.lib.items(title).get()
assert item.title == title
@ -96,7 +97,9 @@ class ModifyTest(BeetsTestCase):
title=f"{title}{i}", artist=original_artist, album=album
)
self.modify_inp(
"s\ny\ny\ny\nn\nn\ny\ny\ny\ny\nn", title, f"artist={new_artist}"
["s", "y", "y", "y", "n", "n", "y", "y", "y", "y", "n"],
title,
f"artist={new_artist}",
)
original_items = self.lib.items(f"artist:{original_artist}")
new_items = self.lib.items(f"artist:{new_artist}")

View file

@ -1,7 +1,7 @@
from beets.test.helper import BeetsTestCase
from beets.test.helper import BeetsTestCase, IOMixin
class WriteTest(BeetsTestCase):
class WriteTest(IOMixin, BeetsTestCase):
def write_cmd(self, *args):
return self.run_with_output("write", *args)

View file

@ -71,11 +71,11 @@ class TestPluginTestCase(PluginTestCase):
plugin = "test"
def setUp(self):
self.config["pluginpath"] = [_common.PLUGINPATH]
super().setUp()
config["pluginpath"] = [_common.PLUGINPATH]
class ConfigTest(TestPluginTestCase):
class ConfigTest(IOMixin, TestPluginTestCase):
def setUp(self):
super().setUp()
@ -162,6 +162,7 @@ class ConfigTest(TestPluginTestCase):
with self.write_config_file() as config:
config.write("library: /xxx/yyy/not/a/real/path")
self.io.addinput("n")
with pytest.raises(ui.UserError):
self.run_command("test", lib=None)
@ -397,7 +398,7 @@ class PluginTest(TestPluginTestCase):
self.run_command("test", lib=None)
class CommonOptionsParserCliTest(BeetsTestCase):
class CommonOptionsParserCliTest(IOMixin, BeetsTestCase):
"""Test CommonOptionsParser and formatting LibModel formatting on 'list'
command.
"""

View file

@ -22,7 +22,7 @@ from random import random
from beets import config, ui
from beets.test import _common
from beets.test.helper import BeetsTestCase, IOMixin, control_stdin
from beets.test.helper import BeetsTestCase, IOMixin
class InputMethodsTest(IOMixin, unittest.TestCase):
@ -85,7 +85,7 @@ class InputMethodsTest(IOMixin, unittest.TestCase):
assert items == ["1", "3"]
class ParentalDirCreation(BeetsTestCase):
class ParentalDirCreation(IOMixin, BeetsTestCase):
def test_create_yes(self):
non_exist_path = _common.os.fsdecode(
os.path.join(self.temp_dir, b"nonexist", str(random()).encode())
@ -94,8 +94,8 @@ class ParentalDirCreation(BeetsTestCase):
# occur; wish I can use a golang defer here.
test_config = deepcopy(config)
test_config["library"] = non_exist_path
with control_stdin("y"):
lib = ui._open_library(test_config)
self.io.addinput("y")
lib = ui._open_library(test_config)
lib._close()
def test_create_no(self):
@ -108,14 +108,14 @@ class ParentalDirCreation(BeetsTestCase):
test_config = deepcopy(config)
test_config["library"] = non_exist_path
with control_stdin("n"):
try:
lib = ui._open_library(test_config)
except ui.UserError:
if os.path.exists(non_exist_path_parent):
shutil.rmtree(non_exist_path_parent)
raise OSError("Parent directories should not be created.")
else:
if lib:
lib._close()
self.io.addinput("n")
try:
lib = ui._open_library(test_config)
except ui.UserError:
if os.path.exists(non_exist_path_parent):
shutil.rmtree(non_exist_path_parent)
raise OSError("Parent directories should not be created.")
else:
if lib:
lib._close()
raise OSError("Parent directories should not be created.")