212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
|
|
"""Tests für die CLI-Commands (cli.py) via typer.testing.CliRunner."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import struct
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
from typer.testing import CliRunner
|
||
|
|
|
||
|
|
from musiksammlung.cli import app
|
||
|
|
from musiksammlung.models import Album, Disc, Track
|
||
|
|
|
||
|
|
runner = CliRunner()
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Hilfsfunktionen (identisch zu test_tagger.py)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
def _make_flac(path: Path) -> Path:
|
||
|
|
data = b"fLaC"
|
||
|
|
data += b"\x80\x00\x00\x22"
|
||
|
|
data += struct.pack(">HH", 4096, 4096)
|
||
|
|
data += b"\x00\x00\x00\x00\x00\x00"
|
||
|
|
val = (44100 << 44) | (0 << 41) | (15 << 36) | 0
|
||
|
|
data += val.to_bytes(8, "big")
|
||
|
|
data += b"\x00" * 16
|
||
|
|
path.write_bytes(data)
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
def _make_album_json(path: Path, *, n_tracks: int = 2, n_discs: int = 1) -> Album:
|
||
|
|
discs = [
|
||
|
|
Disc(
|
||
|
|
disc_number=d,
|
||
|
|
tracks=[
|
||
|
|
Track(track_number=t, title=f"Disc{d} Track{t}") for t in range(1, n_tracks + 1)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
for d in range(1, n_discs + 1)
|
||
|
|
]
|
||
|
|
album = Album(artist="TestArtist", album="TestAlbum", year=2024, discs=discs)
|
||
|
|
path.write_text(album.model_dump_json(indent=2), encoding="utf-8")
|
||
|
|
return album
|
||
|
|
|
||
|
|
|
||
|
|
def _make_disc_files(album_dir: Path, album: Album) -> None:
|
||
|
|
"""Erstellt FLAC-Dateien im korrekten Namensschema."""
|
||
|
|
multi = len(album.discs) > 1
|
||
|
|
for disc in album.discs:
|
||
|
|
disc_dir = album_dir / f"CD{disc.disc_number}" if multi else album_dir
|
||
|
|
disc_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
for track in disc.tracks:
|
||
|
|
fname = f"{track.track_number:02d}_-_{track.title.replace(' ', '_')}_-_TestArtist.flac"
|
||
|
|
_make_flac(disc_dir / fname)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# apply
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
class TestApplyCommand:
|
||
|
|
def test_apply_inplace_renames_and_tags(self, tmp_path: Path) -> None:
|
||
|
|
_make_album_json(tmp_path / "album.json")
|
||
|
|
# Audiodateien mit track*-Namen (wie nach CDDB-Fehler)
|
||
|
|
_make_flac(tmp_path / "track01.flac")
|
||
|
|
_make_flac(tmp_path / "track02.flac")
|
||
|
|
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
|
||
|
|
def test_apply_fails_on_disc_mismatch(self, tmp_path: Path) -> None:
|
||
|
|
_make_album_json(tmp_path / "album.json", n_tracks=3)
|
||
|
|
# Nur 2 Dateien, JSON hat 3 Tracks
|
||
|
|
_make_flac(tmp_path / "track01.flac")
|
||
|
|
_make_flac(tmp_path / "track02.flac")
|
||
|
|
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 1
|
||
|
|
assert "Diskrepanz" in result.output or "Diskrepanz" in (result.stderr or "")
|
||
|
|
|
||
|
|
def test_apply_dry_run_makes_no_changes(self, tmp_path: Path) -> None:
|
||
|
|
_make_album_json(tmp_path / "album.json")
|
||
|
|
f1 = _make_flac(tmp_path / "track01.flac")
|
||
|
|
f2 = _make_flac(tmp_path / "track02.flac")
|
||
|
|
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "album.json"),
|
||
|
|
"--in-place", "--dry-run",
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "DRY-RUN" in result.output
|
||
|
|
# Originaldateien noch vorhanden
|
||
|
|
assert f1.exists()
|
||
|
|
assert f2.exists()
|
||
|
|
|
||
|
|
def test_apply_missing_json_raises(self, tmp_path: Path) -> None:
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "nope.json"), "--in-place",
|
||
|
|
])
|
||
|
|
assert result.exit_code != 0
|
||
|
|
|
||
|
|
def test_apply_creates_playlist(self, tmp_path: Path) -> None:
|
||
|
|
album = _make_album_json(tmp_path / "album.json")
|
||
|
|
_make_disc_files(tmp_path, album)
|
||
|
|
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
playlists = list(tmp_path.glob("*.m3u"))
|
||
|
|
assert len(playlists) == 1
|
||
|
|
|
||
|
|
def test_apply_multi_disc(self, tmp_path: Path) -> None:
|
||
|
|
album = _make_album_json(tmp_path / "album.json", n_tracks=2, n_discs=2)
|
||
|
|
_make_disc_files(tmp_path, album)
|
||
|
|
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"apply", str(tmp_path), str(tmp_path / "album.json"), "--in-place",
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# check
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
class TestCheckCommand:
|
||
|
|
def test_check_shows_cover_status(self, tmp_path: Path) -> None:
|
||
|
|
(tmp_path / "frontcover.jpg").write_bytes(b"\xff\xd8\xff\xe0") # minimal JPEG magic
|
||
|
|
|
||
|
|
result = runner.invoke(app, ["check", str(tmp_path)])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "frontcover.jpg" in result.output
|
||
|
|
|
||
|
|
def test_check_shows_missing_cover(self, tmp_path: Path) -> None:
|
||
|
|
result = runner.invoke(app, ["check", str(tmp_path)])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "fehlt" in result.output
|
||
|
|
|
||
|
|
def test_check_lists_audio_files(self, tmp_path: Path) -> None:
|
||
|
|
_make_flac(tmp_path / "01_-_Track_-_Artist.flac")
|
||
|
|
|
||
|
|
result = runner.invoke(app, ["check", str(tmp_path)])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "01_-_Track_-_Artist.flac" in result.output
|
||
|
|
|
||
|
|
def test_check_nonexistent_directory(self, tmp_path: Path) -> None:
|
||
|
|
result = runner.invoke(app, ["check", str(tmp_path / "nope")])
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
def test_check_no_audio_files_message(self, tmp_path: Path) -> None:
|
||
|
|
result = runner.invoke(app, ["check", str(tmp_path)])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert "Keine Audiodateien" in result.output
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# scan (mockt LLM-Aufruf)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
class TestScanCommand:
|
||
|
|
def test_scan_from_text_creates_json(self, tmp_path: Path) -> None:
|
||
|
|
text_file = tmp_path / "tracklist.txt"
|
||
|
|
text_file.write_text("1. Allegro\n2. Andante", encoding="utf-8")
|
||
|
|
output = tmp_path / "album.json"
|
||
|
|
|
||
|
|
fake_album = Album(
|
||
|
|
artist="Karajan",
|
||
|
|
album="Beethoven",
|
||
|
|
year=1963,
|
||
|
|
discs=[Disc(disc_number=1, tracks=[Track(track_number=1, title="Allegro")])],
|
||
|
|
)
|
||
|
|
|
||
|
|
with patch("musiksammlung.cli.parse_tracklist", return_value=fake_album):
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"scan", "--from-text", str(text_file), "--output", str(output),
|
||
|
|
])
|
||
|
|
|
||
|
|
assert result.exit_code == 0, result.output
|
||
|
|
assert output.exists()
|
||
|
|
data = json.loads(output.read_text())
|
||
|
|
assert data["artist"] == "Karajan"
|
||
|
|
|
||
|
|
def test_scan_missing_text_file(self, tmp_path: Path) -> None:
|
||
|
|
result = runner.invoke(app, [
|
||
|
|
"scan", "--from-text", str(tmp_path / "nope.txt"),
|
||
|
|
])
|
||
|
|
assert result.exit_code == 1
|
||
|
|
|
||
|
|
def test_scan_no_args_fails(self) -> None:
|
||
|
|
result = runner.invoke(app, ["scan"])
|
||
|
|
assert result.exit_code == 1
|