"""Tests for ``alfred.application.subtitles.placer.SubtitlePlacer``. The placer hard-links subtitle files next to a destination video, naming them ``{video_stem}.{lang}[.sdh|.forced].{ext}``. Coverage: - ``TestBuildDestName`` — name construction for standard / SDH / forced; errors on missing language or format. - ``TestPlace`` — happy path: link is created, ``PlacedTrack`` populated. - ``TestSkipReasons`` — embedded, missing source, missing language/format, destination already exists. - ``TestOSError`` — ``os.link`` failures are captured as ``skipped``. - ``TestPlaceResultCounts`` — ``placed_count`` / ``skipped_count`` properties. """ from __future__ import annotations from pathlib import Path from unittest.mock import patch import pytest from alfred.domain.subtitles.entities import SubtitleScanResult from alfred.application.subtitles.placer import ( PlacedTrack, PlaceResult, SubtitlePlacer, _build_dest_name, ) from alfred.domain.subtitles.value_objects import ( SubtitleFormat, SubtitleLanguage, SubtitleType, ) SRT = SubtitleFormat(id="srt", extensions=[".srt"]) ASS = SubtitleFormat(id="ass", extensions=[".ass", ".ssa"]) FRA = SubtitleLanguage(code="fra", tokens=["fr"]) def _track( file_path: Path | None, *, lang=FRA, fmt=SRT, stype=SubtitleType.STANDARD, is_embedded: bool = False, ) -> SubtitleScanResult: return SubtitleScanResult( language=lang, format=fmt, subtitle_type=stype, file_path=file_path, is_embedded=is_embedded, ) # --------------------------------------------------------------------------- # # _build_dest_name # # --------------------------------------------------------------------------- # class TestBuildDestName: def test_standard(self): t = _track(None, stype=SubtitleType.STANDARD) assert _build_dest_name(t, "Movie.2010") == "Movie.2010.fra.srt" def test_sdh(self): t = _track(None, stype=SubtitleType.SDH) assert _build_dest_name(t, "Movie.2010") == "Movie.2010.fra.sdh.srt" def test_forced(self): t = _track(None, stype=SubtitleType.FORCED) assert _build_dest_name(t, "Movie.2010") == "Movie.2010.fra.forced.srt" def test_uses_first_extension_of_multi_ext_format(self): t = _track(None, fmt=ASS) # ASS has [.ass, .ssa] — first wins. assert _build_dest_name(t, "x").endswith(".ass") def test_missing_lang_raises(self): t = _track(None, lang=None) with pytest.raises(ValueError, match="language or format"): _build_dest_name(t, "x") def test_missing_format_raises(self): t = _track(None, fmt=None) with pytest.raises(ValueError, match="language or format"): _build_dest_name(t, "x") # --------------------------------------------------------------------------- # # Place — happy path # # --------------------------------------------------------------------------- # @pytest.fixture def placer(): return SubtitlePlacer() class TestPlace: def test_creates_hard_link_with_correct_name(self, placer, tmp_path): src = tmp_path / "input.srt" src.write_text("subs") video = tmp_path / "lib" / "Movie.2010.mkv" video.parent.mkdir() video.write_bytes(b"") track = _track(src) result = placer.place([track], video) assert result.placed_count == 1 assert result.skipped_count == 0 placed = result.placed[0] assert placed.filename == "Movie.2010.fra.srt" assert placed.destination.exists() # Hard link → same inode as source. assert placed.destination.stat().st_ino == src.stat().st_ino def test_multiple_tracks_distinct_destinations(self, placer, tmp_path): s1 = tmp_path / "a.srt" s1.write_text("") s2 = tmp_path / "b.srt" s2.write_text("") video = tmp_path / "lib" / "Movie.mkv" video.parent.mkdir() video.write_bytes(b"") ENG = SubtitleLanguage(code="eng", tokens=["en"]) t1 = _track(s1, lang=FRA) t2 = _track(s2, lang=ENG, stype=SubtitleType.SDH) result = placer.place([t1, t2], video) assert result.placed_count == 2 names = {p.filename for p in result.placed} assert names == {"Movie.fra.srt", "Movie.eng.sdh.srt"} # --------------------------------------------------------------------------- # # Skip reasons # # --------------------------------------------------------------------------- # class TestSkipReasons: def test_embedded_skipped(self, placer, tmp_path): video = tmp_path / "Movie.mkv" video.write_bytes(b"") track = _track(None, is_embedded=True) result = placer.place([track], video) assert result.placed == [] assert len(result.skipped) == 1 assert "embedded" in result.skipped[0][1] def test_missing_source_file(self, placer, tmp_path): video = tmp_path / "Movie.mkv" video.write_bytes(b"") track = _track(tmp_path / "ghost.srt") result = placer.place([track], video) assert result.placed == [] assert "not found" in result.skipped[0][1] def test_missing_lang_or_format_skipped(self, placer, tmp_path): video = tmp_path / "Movie.mkv" video.write_bytes(b"") src = tmp_path / "x.srt" src.write_text("") track = _track(src, lang=None) result = placer.place([track], video) assert result.placed == [] assert "language or format" in result.skipped[0][1] def test_destination_already_exists(self, placer, tmp_path): src = tmp_path / "x.srt" src.write_text("a") video = tmp_path / "lib" / "Movie.mkv" video.parent.mkdir() video.write_bytes(b"") # Pre-create destination (video.parent / "Movie.fra.srt").write_text("preexisting") track = _track(src) result = placer.place([track], video) assert result.placed == [] assert "already exists" in result.skipped[0][1] # --------------------------------------------------------------------------- # # OSError handling # # --------------------------------------------------------------------------- # class TestOSError: def test_link_failure_captured_as_skipped(self, placer, tmp_path): src = tmp_path / "x.srt" src.write_text("") video = tmp_path / "lib" / "Movie.mkv" video.parent.mkdir() video.write_bytes(b"") track = _track(src) with patch( "alfred.application.subtitles.placer.os.link", side_effect=OSError("cross-device link"), ): result = placer.place([track], video) assert result.placed == [] assert "cross-device" in result.skipped[0][1] # --------------------------------------------------------------------------- # # PlaceResult counters # # --------------------------------------------------------------------------- # class TestPlaceResultCounts: def test_counts(self): # Synthesize a PlaceResult directly for property check. pt = PlacedTrack(source=Path("/a"), destination=Path("/b"), filename="b") st = _track(None, is_embedded=True) r = PlaceResult(placed=[pt], skipped=[(st, "x")]) assert r.placed_count == 1 assert r.skipped_count == 1