Music_Metadata_Enricher/test_suite_enricher.py
dschlueter 787803bb7b Fix file permissions after rebase (644 → 755)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 06:01:38 +02:00

274 lines
11 KiB
Python
Executable file

#!/usr/bin/env python3
"""test_suite_enricher.py — Unit- und Integrationstests für music_enricher."""
from __future__ import annotations
import sys
import tempfile
import traceback
from pathlib import Path
from typing import Callable
sys.path.insert(0, str(Path(__file__).parent))
from models import AlbumScan, TrackHints, AlbumHints
RESULTS: list[dict] = []
def record(test_id: str, passed: bool, detail: str = "") -> None:
RESULTS.append({"id": test_id, "status": "PASS" if passed else "FAIL", "detail": detail})
def run_case(test_id: str, fn: Callable[[], str]) -> None:
try:
detail = fn()
record(test_id, True, detail)
except Exception:
record(test_id, False, traceback.format_exc()[:300])
# ---------------------------------------------------------------------------
# hint_extractor Tests
# ---------------------------------------------------------------------------
def test_parse_dirname_artist_album() -> str:
from hint_extractor import _parse_dirname
artist, album, year = _parse_dirname("Pink_Floyd_-_The_Wall")
assert artist and "Pink" in artist, f"artist: {artist}"
assert album and "Wall" in album, f"album: {album}"
return f"artist={artist!r}, album={album!r}"
def test_parse_dirname_with_year() -> str:
from hint_extractor import _parse_dirname
artist, album, year = _parse_dirname("Abba_-_Greatest_Hits_1992")
assert year == "1992", f"year: {year}"
return f"year={year}"
def test_parse_dirname_album_only() -> str:
from hint_extractor import _parse_dirname
artist, album, year = _parse_dirname("Beethoven_Complete_Edition")
assert album is not None, "album should not be None"
return f"album={album!r}"
def test_parse_filename_track_artist_title() -> str:
from hint_extractor import _parse_filename
r = _parse_filename("07 - ABBA - Dancing Queen")
assert r.get("track") == "07", f"track: {r}"
assert "ABBA" in r.get("artist", ""), f"artist: {r}"
assert "Dancing" in r.get("title", ""), f"title: {r}"
return str(r)
def test_parse_filename_disc_track_title() -> str:
from hint_extractor import _parse_filename
r = _parse_filename("2-07 - Bach - Toccata")
assert r.get("disc") == "2", f"disc: {r}"
assert r.get("track") == "07", f"track: {r}"
return str(r)
def test_parse_filename_track_title() -> str:
from hint_extractor import _parse_filename
r = _parse_filename("01 - Dancing Queen")
assert r.get("track") == "01", f"track: {r}"
assert "Dancing" in r.get("title", ""), f"title: {r}"
return str(r)
def test_parse_filename_artist_title() -> str:
from hint_extractor import _parse_filename
r = _parse_filename("Miles Davis - So What")
assert "Miles" in r.get("artist", ""), f"artist: {r}"
assert "What" in r.get("title", ""), f"title: {r}"
return str(r)
def test_parse_tracklist_numbered() -> str:
from hint_extractor import _parse_tracklist
text = "1. Dancing Queen\n2. Waterloo\n3. Fernando"
tracks = _parse_tracklist(text)
assert len(tracks) == 3, f"count: {len(tracks)}"
assert tracks[0]["title"] == "Dancing Queen", f"title: {tracks[0]}"
return f"{len(tracks)} tracks parsed"
def test_parse_tracklist_with_duration() -> str:
from hint_extractor import _parse_tracklist
text = "1-1 Toccata And Fugue 9:17\n1-2 Heartbeat 2:19\n2-1 Finale 5:00"
tracks = _parse_tracklist(text)
assert len(tracks) >= 2, f"count: {len(tracks)}"
assert tracks[0]["disc"] == "1", f"disc: {tracks[0]}"
return f"{len(tracks)} tracks parsed"
def test_parse_tracklist_with_disc_sections() -> str:
from hint_extractor import _parse_tracklist
text = "CD 1\n1. Track A\n2. Track B\nCD 2\n1. Track C"
tracks = _parse_tracklist(text)
disc2 = [t for t in tracks if t.get("disc") == "2"]
assert len(disc2) >= 1, f"disc2: {disc2}"
return f"{len(tracks)} total, {len(disc2)} on disc 2"
# ---------------------------------------------------------------------------
# Scanner Tests
# ---------------------------------------------------------------------------
def test_scanner_classifies_files() -> str:
from scanner import scan_album
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "TestAlbum"
root.mkdir()
(root / "01 - Song.mp3").write_bytes(b"\x00" * 100)
(root / "02 - Song.flac").write_bytes(b"\x00" * 100)
(root / "front.jpg").write_bytes(b"\xff\xd8" + b"\x00" * 100)
(root / "tracklist.txt").write_text("1. Track One\n2. Track Two")
(root / "notes.pdf").write_bytes(b"\x00" * 50)
scan = scan_album(root)
assert len(scan.audio_files) == 2, f"audio: {scan.audio_files}"
assert len(scan.image_files) == 1, f"images: {scan.image_files}"
assert len(scan.tracklist_files) == 1, f"tracklists: {scan.tracklist_files}"
return "scan OK: 2 audio, 1 image, 1 tracklist"
def test_scanner_ignores_hidden() -> str:
from scanner import scan_album
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "Album"
root.mkdir()
(root / "song.mp3").write_bytes(b"\x00" * 100)
(root / ".hidden.mp3").write_bytes(b"\x00" * 100)
(root / "_trash.mp3").write_bytes(b"\x00" * 100)
scan = scan_album(root)
assert len(scan.audio_files) == 1, f"should ignore hidden: {scan.audio_files}"
return "hidden files correctly ignored"
# ---------------------------------------------------------------------------
# extract_hints integration
# ---------------------------------------------------------------------------
def test_extract_hints_from_scan() -> str:
from scanner import scan_album
from hint_extractor import extract_hints
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "ABBA_-_Greatest_Hits"
root.mkdir()
(root / "01 - ABBA - Dancing Queen.mp3").write_bytes(b"\x00" * 1024)
(root / "02 - ABBA - Waterloo.mp3").write_bytes(b"\x00" * 1024)
(root / "tracklist.txt").write_text("1. Dancing Queen\n2. Waterloo\n")
scan = scan_album(root)
hints = extract_hints(scan)
assert hints.dir_album is not None, "album hint missing"
assert len(hints.tracks) == 2, f"tracks: {len(hints.tracks)}"
assert hints.tracklist_text is not None, "tracklist not read"
return f"hints OK: album={hints.dir_album!r}, {len(hints.tracks)} tracks"
def test_extract_hints_multi_disc() -> str:
from scanner import scan_album
from hint_extractor import extract_hints
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "Bach_Complete"
(root / "CD1").mkdir(parents=True)
(root / "CD2").mkdir()
(root / "CD1" / "01 - Toccata.mp3").write_bytes(b"\x00" * 1024)
(root / "CD2" / "01 - Fugue.mp3").write_bytes(b"\x00" * 1024)
scan = scan_album(root)
hints = extract_hints(scan)
disc_nums = {t.disc_number for t in hints.tracks if t.disc_number}
assert 1 in disc_nums, f"disc 1 missing: {disc_nums}"
assert 2 in disc_nums, f"disc 2 missing: {disc_nums}"
return f"disc numbers detected: {disc_nums}"
# ---------------------------------------------------------------------------
# executor Tests
# ---------------------------------------------------------------------------
def test_proposed_filename_single_disc() -> str:
from executor import _proposed_filename
from models import TrackProposal
from pathlib import Path
# Pop schema: albumartist == track artist → TT_-_Artist_-_Title
tp = TrackProposal(path=Path("dummy.mp3"), title="Dancing Queen",
artist="ABBA", track_number=1, disc_number=None)
name = _proposed_filename(tp, ".mp3", albumartist="ABBA")
assert name == "01_-_ABBA_-_Dancing_Queen.mp3", f"got: {name!r}"
return name
def test_proposed_filename_multi_disc() -> str:
from executor import _proposed_filename
from models import TrackProposal
from pathlib import Path
# Classical schema: albumartist (performer) ≠ track artist (composer)
tp = TrackProposal(path=Path("dummy.flac"), title="Toccata",
artist="Bach", track_number=7, disc_number=2)
name = _proposed_filename(tp, ".flac", albumartist="Gardiner")
assert name == "2-07_-_Gardiner_-_Bach_-_Toccata.flac", f"got: {name!r}"
return name
def test_proposed_filename_sanitizes_chars() -> str:
from executor import _proposed_filename
from models import TrackProposal
from pathlib import Path
tp = TrackProposal(path=Path("x.mp3"), title='Track: "Live" / Today',
artist="Artist?", track_number=3, disc_number=None)
name = _proposed_filename(tp, ".mp3")
assert "/" not in name and ":" not in name, f"unsafe chars in: {name!r}"
return name
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
def main() -> None:
print("🧪 Starte Music Metadata Enricher Tests...")
cases = [
("UNIT_01_parse_dirname_artist_album", test_parse_dirname_artist_album),
("UNIT_02_parse_dirname_with_year", test_parse_dirname_with_year),
("UNIT_03_parse_dirname_album_only", test_parse_dirname_album_only),
("UNIT_04_parse_filename_track_artist_title", test_parse_filename_track_artist_title),
("UNIT_05_parse_filename_disc_track_title", test_parse_filename_disc_track_title),
("UNIT_06_parse_filename_track_title", test_parse_filename_track_title),
("UNIT_07_parse_filename_artist_title", test_parse_filename_artist_title),
("UNIT_08_parse_tracklist_numbered", test_parse_tracklist_numbered),
("UNIT_09_parse_tracklist_with_duration", test_parse_tracklist_with_duration),
("UNIT_10_parse_tracklist_disc_sections", test_parse_tracklist_with_disc_sections),
("UNIT_11_scanner_classifies_files", test_scanner_classifies_files),
("UNIT_12_scanner_ignores_hidden", test_scanner_ignores_hidden),
("UNIT_13_extract_hints_from_scan", test_extract_hints_from_scan),
("UNIT_14_extract_hints_multi_disc", test_extract_hints_multi_disc),
("UNIT_15_proposed_filename_single_disc", test_proposed_filename_single_disc),
("UNIT_16_proposed_filename_multi_disc", test_proposed_filename_multi_disc),
("UNIT_17_proposed_filename_sanitizes_chars", test_proposed_filename_sanitizes_chars),
]
for test_id, fn in cases:
run_case(test_id, fn)
print("=" * 70)
for r in RESULTS:
icon = "" if r["status"] == "PASS" else ""
detail = r["detail"][:100] + "..." if len(r["detail"]) > 100 else r["detail"]
print(f"{icon} [{r['status']}] {r['id']} {detail}")
print("=" * 70)
passed = sum(1 for r in RESULTS if r["status"] == "PASS")
total = len(RESULTS)
print(f"📊 {passed}/{total} Tests erfolgreich")
sys.exit(0 if passed == total else 1)
if __name__ == "__main__":
main()