mirror of
https://github.com/beetbox/beets.git
synced 2026-02-09 00:41:57 +01:00
Replace custom stdio mocks with pytest io fixture
Create a centralised pytest fixture to provide controllable stdin and captured stdout in all tests. Simplify DummyIO/DummyIn and remove the custom DummyOut implementation and make use of pytest builtin fixtures. Create a centralised pytest fixture to provide controllable stdin and captured stdout that can be applied to any tests, regardless whether they are based on pytest or unittest. * `io` fixture can be used as a fixture in pytest-based tests * `IOMixin` can be used to attach `io` attribute to any test class, including `unittest.TestCase`
This commit is contained in:
parent
125313d539
commit
ed43387778
4 changed files with 62 additions and 67 deletions
|
|
@ -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,77 +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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ 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
|
||||
|
||||
|
|
@ -163,19 +164,9 @@ NEEDS_REFLINK = unittest.skipUnless(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("io")
|
||||
class IOMixin:
|
||||
@cached_property
|
||||
def io(self) -> _common.DummyIO:
|
||||
return _common.DummyIO()
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
patcher = patch.multiple(
|
||||
"sys", stdin=self.io.stdin, stdout=self.io.stdout
|
||||
)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
io: _common.DummyIO
|
||||
|
||||
|
||||
class TestHelper(ConfigMixin):
|
||||
|
|
@ -757,8 +748,6 @@ 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:
|
||||
return TerminalImportSessionFixture(
|
||||
self.lib,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue