beets/test/plugins/test_replace.py
2026-03-08 17:27:07 +00:00

254 lines
7.5 KiB
Python

import optparse
import shutil
from collections.abc import Generator
from pathlib import Path
from unittest.mock import Mock
import pytest
from mediafile import MediaFile
from beets import ui
from beets.library import Item, Library
from beets.library.exceptions import WriteError
from beets.test import _common
from beets.test.helper import TestHelper
from beetsplug.replace import ReplacePlugin
replace = ReplacePlugin()
def always(x):
return lambda *args, **kwargs: x
def always_raise(x):
def err(*args, **kwargs):
raise x
return err
class TestReplace:
@pytest.fixture
def mp3_file(self, tmp_path) -> Path:
dest = tmp_path / "full.mp3"
src = Path(_common.RSRC.decode()) / "full.mp3"
shutil.copyfile(src, dest)
mediafile = MediaFile(dest)
mediafile.title = "AAA"
mediafile.save()
return dest
@pytest.fixture
def opus_file(self, tmp_path) -> Path:
dest = tmp_path / "full.opus"
src = Path(_common.RSRC.decode()) / "full.opus"
shutil.copyfile(src, dest)
return dest
@pytest.fixture
def library(self) -> Generator[Library]:
helper = TestHelper()
helper.setup_beets()
yield helper.lib
helper.teardown_beets()
def test_run_replace_too_few_args(self):
with pytest.raises(ui.UserError):
replace.run(None, optparse.Values(), [])
def test_run_replace_no_matches(self, library):
with pytest.raises(ui.UserError):
replace.run(library, optparse.Values(), ["BBB", ""])
def test_run_replace_no_song_selected(
self, library, mp3_file, opus_file, monkeypatch
):
monkeypatch.setattr(replace, "file_check", always(None))
monkeypatch.setattr(replace, "select_song", always(None))
item = Item.from_path(mp3_file)
library.add(item)
replace.run(library, optparse.Values(), ["AAA", str(opus_file)])
assert mp3_file.exists()
assert opus_file.exists()
def test_run_replace_not_confirmed(
self, library, mp3_file, opus_file, monkeypatch
):
monkeypatch.setattr(replace, "file_check", always(None))
monkeypatch.setattr(replace, "confirm_replacement", always(False))
item = Item.from_path(mp3_file)
library.add(item)
monkeypatch.setattr(replace, "select_song", always(item))
replace.run(library, optparse.Values(), ["AAA", str(opus_file)])
assert mp3_file.exists()
assert opus_file.exists()
def test_run_replace(self, library, mp3_file, opus_file, monkeypatch):
replace_file = Mock(replace.replace_file, return_value=None)
monkeypatch.setattr(replace, "replace_file", replace_file)
monkeypatch.setattr(replace, "file_check", always(None))
monkeypatch.setattr(replace, "confirm_replacement", always(True))
item = Item.from_path(mp3_file)
library.add(item)
monkeypatch.setattr(replace, "select_song", always(item))
replace.run(library, optparse.Values(), ["AAA", str(opus_file)])
replace_file.assert_called_once()
def test_path_is_dir(self, tmp_path):
fake_directory = tmp_path / "fakeDir"
fake_directory.mkdir()
with pytest.raises(ui.UserError):
replace.file_check(fake_directory)
def test_path_is_unsupported_file(self, tmp_path):
fake_file = tmp_path / "fakefile.txt"
fake_file.write_text("test", encoding="utf-8")
with pytest.raises(ui.UserError):
replace.file_check(fake_file)
def test_path_is_supported_file(self, mp3_file):
replace.file_check(mp3_file)
def test_select_song_valid_choice(self, monkeypatch, capfd):
songs = ["Song A", "Song B", "Song C"]
monkeypatch.setattr("builtins.input", lambda _: "2")
selected_song = replace.select_song(songs)
captured = capfd.readouterr()
assert "1. Song A" in captured.out
assert "2. Song B" in captured.out
assert "3. Song C" in captured.out
assert selected_song == "Song B"
def test_select_song_cancel(self, monkeypatch):
songs = ["Song A", "Song B", "Song C"]
monkeypatch.setattr("builtins.input", lambda _: "0")
selected_song = replace.select_song(songs)
assert selected_song is None
def test_select_song_invalid_then_valid(self, monkeypatch, capfd):
songs = ["Song A", "Song B", "Song C"]
inputs = iter(["invalid", "4", "3"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
selected_song = replace.select_song(songs)
captured = capfd.readouterr()
assert "Invalid input. Please type in a number." in captured.out
assert (
"Invalid choice. Please enter a number between 1 and 3."
in captured.out
)
assert selected_song == "Song C"
def test_confirm_replacement_file_not_exist(self):
class Song:
path = b"test123321.txt"
song = Song()
with pytest.raises(ui.UserError):
replace.confirm_replacement("test", song)
def test_confirm_replacement_yes(self, monkeypatch):
src = Path(_common.RSRC.decode()) / "full.mp3"
monkeypatch.setattr("builtins.input", lambda _: "YES ")
class Song:
path = str(src).encode()
song = Song()
assert replace.confirm_replacement("test", song) is True
def test_confirm_replacement_no(self, monkeypatch):
src = Path(_common.RSRC.decode()) / "full.mp3"
monkeypatch.setattr("builtins.input", lambda _: "test123")
class Song:
path = str(src).encode()
song = Song()
assert replace.confirm_replacement("test", song) is False
def test_replace_file_move_fails(self, tmp_path):
item = Item()
item.path = bytes(tmp_path / "not_a_song.mp3")
with pytest.raises(ui.UserError):
replace.replace_file(tmp_path / "not_a_file.opus", item)
def test_replace_file_delete_fails(
self, library, mp3_file, opus_file, monkeypatch
):
monkeypatch.setattr(Path, "unlink", always_raise(OSError))
item = Item.from_path(mp3_file)
library.add(item)
with pytest.raises(ui.UserError):
replace.replace_file(opus_file, item)
def test_replace_file_write_fails(
self, library, mp3_file, opus_file, monkeypatch
):
monkeypatch.setattr(
Item, "write", always_raise(WriteError("path", "reason"))
)
item = Item.from_path(mp3_file)
library.add(item)
with pytest.raises(ui.UserError):
replace.replace_file(opus_file, item)
def test_replace_file(
self, mp3_file: Path, opus_file: Path, library: Library
):
old_mediafile = MediaFile(mp3_file)
old_mediafile.albumartist = "ABC"
old_mediafile.disctitle = "DEF"
old_mediafile.genre = "GHI"
old_mediafile.save()
item = Item.from_path(mp3_file)
library.add(item)
replace.replace_file(opus_file, item)
# Check that the file has been replaced.
assert opus_file.exists()
assert not mp3_file.exists()
# Check that the database path has been updated.
assert item.path == bytes(opus_file)
# Check that the new file has the old file's metadata.
new_mediafile = MediaFile(opus_file)
assert new_mediafile.albumartist == "ABC"
assert new_mediafile.disctitle == "DEF"
assert new_mediafile.genre == "GHI"