mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
214 lines
6.4 KiB
Python
214 lines
6.4 KiB
Python
"""Unit tests for the shared music rendering helpers in unshackle.core.music.display.
|
|
|
|
Rendering is visual, so these assert structure/type/no-crash rather than exact
|
|
ANSI. No network: the artwork soft-fail path is exercised with a session whose
|
|
get raises, and (when Pillow is absent) via the missing-dependency guard.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from rich.console import Console, RenderableType
|
|
from rich.panel import Panel
|
|
|
|
from unshackle.core.music.display import (
|
|
MusicHeaderInfo,
|
|
TrackRow,
|
|
format_track_detail_quality,
|
|
render_album_header,
|
|
render_artwork_preview,
|
|
render_track_panel,
|
|
)
|
|
from unshackle.core.titles.music import Song
|
|
|
|
|
|
class DummyService:
|
|
"""Stand-in service class; Song only requires a type, never an instance."""
|
|
|
|
|
|
def make_song(*, track: int = 1, name: str = "Song", album: str = "Album") -> Song:
|
|
return Song(
|
|
id_=f"{album}-{track}",
|
|
service=DummyService,
|
|
name=name,
|
|
artist="Artist",
|
|
album=album,
|
|
track=track,
|
|
disc=1,
|
|
year=2020,
|
|
)
|
|
|
|
|
|
def render_to_text(renderable: RenderableType) -> str:
|
|
"""Render a rich renderable to plain text so we can assert on its content."""
|
|
console = Console(width=120, record=True, force_terminal=False)
|
|
console.print(renderable)
|
|
return console.export_text()
|
|
|
|
|
|
def test_trackrow_constructs() -> None:
|
|
song = make_song(name="Track A")
|
|
row = TrackRow(
|
|
song=song,
|
|
quality_label="FLAC 16-bit/44.1kHz",
|
|
layout="Stereo",
|
|
duration_str="3:21",
|
|
hires=True,
|
|
cd=True,
|
|
note="",
|
|
)
|
|
assert row.song is song
|
|
assert row.quality_label == "FLAC 16-bit/44.1kHz"
|
|
assert row.layout == "Stereo"
|
|
assert row.hires is True
|
|
assert row.cd is True
|
|
assert row.note == ""
|
|
|
|
|
|
def test_trackrow_defaults() -> None:
|
|
row = TrackRow(song=make_song(), quality_label="MP3 320", layout="Stereo", duration_str="2:00")
|
|
assert row.hires is False
|
|
assert row.cd is False
|
|
assert row.note == ""
|
|
|
|
|
|
def test_musicheaderinfo_constructs() -> None:
|
|
info = MusicHeaderInfo(
|
|
artist="Some Artist",
|
|
album="Some Album",
|
|
year="2021",
|
|
track_count=12,
|
|
quality_label="FLAC 24-bit/96kHz",
|
|
duration_str="42:10",
|
|
)
|
|
assert info.artist == "Some Artist"
|
|
assert info.album == "Some Album"
|
|
assert info.year == "2021"
|
|
assert info.track_count == 12
|
|
assert info.quality_label == "FLAC 24-bit/96kHz"
|
|
assert info.duration_str == "42:10"
|
|
assert info.artist_label == "Artist"
|
|
|
|
|
|
def test_format_track_detail_quality_brackets_codec() -> None:
|
|
assert format_track_detail_quality("FLAC 16-bit/44.1kHz") == "[FLAC] | 16-bit/44.1kHz"
|
|
assert format_track_detail_quality("ogg 320kbps") == "[OGG] | 320kbps"
|
|
|
|
|
|
def test_format_track_detail_quality_passthrough() -> None:
|
|
assert format_track_detail_quality("Lossless") == "Lossless"
|
|
assert format_track_detail_quality("") == ""
|
|
|
|
|
|
def test_render_track_panel_returns_panel() -> None:
|
|
rows = [
|
|
TrackRow(
|
|
song=make_song(track=1, name="First"),
|
|
quality_label="FLAC 16-bit/44.1kHz",
|
|
layout="Stereo",
|
|
duration_str="3:00",
|
|
cd=True,
|
|
),
|
|
TrackRow(
|
|
song=make_song(track=2, name="Second"),
|
|
quality_label="OGG 320",
|
|
layout="Stereo",
|
|
duration_str="4:00",
|
|
hires=True,
|
|
),
|
|
]
|
|
panel = render_track_panel(rows, total=len(rows))
|
|
assert isinstance(panel, Panel)
|
|
|
|
text = render_to_text(panel)
|
|
assert "First" in text
|
|
assert "Second" in text
|
|
assert "2 Tracks" in text
|
|
assert "CD" in text
|
|
assert "Hi-Res" in text
|
|
|
|
|
|
def test_render_track_panel_singular_label() -> None:
|
|
rows = [
|
|
TrackRow(song=make_song(name="Only"), quality_label="FLAC 16-bit/44.1kHz", layout="Stereo", duration_str="3:00")
|
|
]
|
|
text = render_to_text(render_track_panel(rows, total=1))
|
|
# The count node reads "1 Track" (singular); the panel title is always
|
|
# "Available Tracks", so we only check the count node phrasing here.
|
|
assert "1 Track" in text
|
|
assert "1 Tracks" not in text
|
|
|
|
|
|
def test_render_track_panel_note_marks_unavailable() -> None:
|
|
rows = [
|
|
TrackRow(
|
|
song=make_song(name="Gone"),
|
|
quality_label="FLAC",
|
|
layout="Stereo",
|
|
duration_str="3:00",
|
|
cd=True,
|
|
hires=True,
|
|
note="(unavailable in region)",
|
|
)
|
|
]
|
|
text = render_to_text(render_track_panel(rows, total=1))
|
|
assert "unavailable in region" in text
|
|
# When a note is present, the badges are suppressed.
|
|
assert "CD" not in text
|
|
assert "Hi-Res" not in text
|
|
|
|
|
|
def test_render_album_header_without_artwork() -> None:
|
|
info = MusicHeaderInfo(
|
|
artist="The Artist",
|
|
album="Greatest Hits",
|
|
year="1999",
|
|
track_count=10,
|
|
quality_label="FLAC 16-bit/44.1kHz",
|
|
duration_str="55:00",
|
|
)
|
|
header = render_album_header(info)
|
|
assert header is not None
|
|
text = render_to_text(header)
|
|
assert "The Artist" in text
|
|
assert "Greatest Hits" in text
|
|
assert "1999" in text
|
|
assert "10" in text
|
|
|
|
|
|
def test_render_album_header_with_artwork() -> None:
|
|
info = MusicHeaderInfo(
|
|
artist="Owner Name",
|
|
album="A Playlist",
|
|
year="2024",
|
|
track_count=3,
|
|
quality_label="OGG 320",
|
|
artist_label="Owner",
|
|
)
|
|
artwork = render_album_header(MusicHeaderInfo("x", "y", "1", 1, "q")) # any renderable
|
|
header = render_album_header(info, artwork=artwork)
|
|
assert header is not None
|
|
text = render_to_text(header)
|
|
assert "Owner Name" in text
|
|
assert "A Playlist" in text
|
|
assert "Owner" in text
|
|
|
|
|
|
class RaisingSession:
|
|
"""Session stub whose get() always raises, to drive the soft-fail path."""
|
|
|
|
def get(self, *args: Any, **kwargs: Any) -> Any:
|
|
raise RuntimeError("network disabled in tests")
|
|
|
|
|
|
def test_render_artwork_preview_empty_url_returns_none() -> None:
|
|
assert render_artwork_preview(RaisingSession(), "") is None
|
|
|
|
|
|
def test_render_artwork_preview_soft_fails_without_network() -> None:
|
|
# Either Pillow is absent (guard returns None) or the session.get raises
|
|
# (caught and returns None). Both paths must yield None, never raise.
|
|
result: Optional[RenderableType] = render_artwork_preview(RaisingSession(), "https://example.invalid/cover.jpg")
|
|
assert result is None
|