Ctrl K
ComunidadGuías…

backend/tests/test_forced_detection.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Tests for forced_detection module.

Covers: detect_subtitle_type (multi-signal), is_forced_external_sub,
classify_forced_result, SUBTITLE_TYPES constant.
"""

import pytest

from forced_detection import (
    SUBTITLE_TYPES,
    classify_forced_result,
    detect_subtitle_type,
    is_forced_external_sub,
)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


class TestConstants:
    def test_subtitle_types_contains_expected(self):
        assert "full" in SUBTITLE_TYPES
        assert "forced" in SUBTITLE_TYPES
        assert "signs" in SUBTITLE_TYPES

    def test_subtitle_types_length(self):
        assert len(SUBTITLE_TYPES) == 3


# ---------------------------------------------------------------------------
# detect_subtitle_type — single signals
# ---------------------------------------------------------------------------


class TestDetectSubtitleTypeSingleSignals:
    def test_no_signals_returns_full(self):
        """No arguments at all -> full with confidence 1.0."""
        stype, conf = detect_subtitle_type()
        assert stype == "full"
        assert conf == 1.0

    def test_disposition_forced(self):
        """ffprobe disposition.forced=1 -> forced, confidence 1.0."""
        stream = {"disposition": {"forced": 1}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "forced"
        assert conf == 1.0

    def test_disposition_not_forced(self):
        """disposition.forced=0 -> no signal, falls through to full."""
        stream = {"disposition": {"forced": 0}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "full"
        assert conf == 1.0

    def test_disposition_missing(self):
        """No disposition key -> no signal."""
        stream = {"tags": {"language": "eng"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "full"
        assert conf == 1.0

    def test_filename_forced(self):
        """.forced. in filename -> forced, 0.9."""
        stype, conf = detect_subtitle_type(file_path="/media/show.forced.srt")
        assert stype == "forced"
        assert conf == 0.9

    def test_filename_foreign(self):
        """.foreign. in filename -> forced, 0.9."""
        stype, conf = detect_subtitle_type(file_path="/media/show.foreign.ass")
        assert stype == "forced"
        assert conf == 0.9

    def test_filename_signs(self):
        """.signs. in filename -> signs, 0.9."""
        stype, conf = detect_subtitle_type(file_path="/media/show.signs.srt")
        assert stype == "signs"
        assert conf == 0.9

    def test_filename_sign_singular(self):
        """.sign. in filename -> signs, 0.9."""
        stype, conf = detect_subtitle_type(file_path="/media/show.sign.ass")
        assert stype == "signs"
        assert conf == 0.9

    def test_stream_title_forced(self):
        """Stream title containing 'forced' -> forced, 0.8."""
        stream = {"tags": {"title": "English (Forced)"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "forced"
        assert conf == 0.8

    def test_stream_title_foreign(self):
        """Stream title containing 'foreign' -> forced, 0.8."""
        stream = {"tags": {"title": "Foreign Parts Only"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "forced"
        assert conf == 0.8

    def test_stream_title_sign(self):
        """Stream title containing 'sign' -> signs, 0.8."""
        stream = {"tags": {"title": "Signs/Songs"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "signs"
        assert conf == 0.8

    def test_stream_title_song(self):
        """Stream title containing 'song' -> signs, 0.8."""
        stream = {"tags": {"title": "Songs Only"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "signs"
        assert conf == 0.8

    def test_empty_stream_info(self):
        """Empty stream_info dict -> full."""
        stype, conf = detect_subtitle_type(stream_info={})
        assert stype == "full"
        assert conf == 1.0

    def test_stream_title_none(self):
        """tags.title is None -> no signal."""
        stream = {"tags": {"title": None}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "full"

    def test_tags_not_dict(self):
        """tags is not a dict -> no signal from title."""
        stream = {"tags": "invalid"}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "full"

    def test_disposition_not_dict(self):
        """disposition is not a dict -> no signal."""
        stream = {"disposition": "invalid"}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "full"


# ---------------------------------------------------------------------------
# detect_subtitle_type — multi-signal agreement
# ---------------------------------------------------------------------------


class TestDetectSubtitleTypeMultiSignal:
    def test_disposition_plus_filename_forced(self):
        """Two forced signals -> forced with highest confidence."""
        stream = {"disposition": {"forced": 1}}
        stype, conf = detect_subtitle_type(stream_info=stream, file_path="/media/show.forced.srt")
        assert stype == "forced"
        assert conf == 1.0  # highest confidence from disposition

    def test_filename_plus_title_signs(self):
        """Filename signs + title sign -> signs with 0.9 (filename confidence)."""
        stream = {"tags": {"title": "Signs/Songs"}}
        stype, conf = detect_subtitle_type(stream_info=stream, file_path="/media/show.signs.srt")
        assert stype == "signs"
        assert conf == 0.9

    def test_mixed_signals_forced_wins_by_count(self):
        """Disposition forced + title forced -> forced wins by count."""
        stream = {"disposition": {"forced": 1}, "tags": {"title": "Forced"}}
        stype, conf = detect_subtitle_type(stream_info=stream)
        assert stype == "forced"
        assert conf == 1.0

    def test_filename_forced_and_signs(self):
        """Filename with both .forced. and .signs. -> both signals, single signal each."""
        stype, conf = detect_subtitle_type(file_path="/media/show.forced.signs.srt")
        # Both forced and signs get one signal each, highest confidence wins
        assert stype in ("forced", "signs")
        assert conf == 0.9

    def test_ass_signs_only_styles(self):
        """ASS with only signs styles -> signs, 0.7."""
        mock_ass = type("SSAFile", (), {"styles": {}})()
        import unittest.mock as mock

        with mock.patch("ass_utils.classify_styles") as m:
            # No dialog styles, only signs styles
            m.return_value = ([], ["Sign", "Title"])
            stype, conf = detect_subtitle_type(ass_content=mock_ass)
            assert stype == "signs"
            assert conf == 0.7

    def test_ass_with_dialog_styles(self):
        """ASS with dialog styles -> not signs from ASS signal."""
        mock_ass = type("SSAFile", (), {"styles": {}})()
        import unittest.mock as mock

        with mock.patch("ass_utils.classify_styles") as m:
            m.return_value = (["Default", "Main"], ["Sign"])
            stype, conf = detect_subtitle_type(ass_content=mock_ass)
            assert stype == "full"
            assert conf == 1.0

    def test_ass_classify_exception(self):
        """classify_styles raises -> no ASS signal, no crash."""
        mock_ass = type("SSAFile", (), {"styles": {}})()
        import unittest.mock as mock

        with mock.patch("ass_utils.classify_styles", side_effect=Exception("parse error")):
            stype, conf = detect_subtitle_type(ass_content=mock_ass)
            assert stype == "full"
            assert conf == 1.0


# ---------------------------------------------------------------------------
# is_forced_external_sub
# ---------------------------------------------------------------------------


class TestIsForcedExternalSub:
    @pytest.mark.parametrize(
        "path,expected",
        [
            ("/media/show.forced.srt", True),
            ("/media/show.forced.ass", True),
            ("/media/show.signs.srt", True),
            ("/media/show.sign.ass", True),
            ("/media/show.foreign.vtt", True),
            ("/media/show.forced.ssa", True),
            ("/media/show.en.srt", False),
            ("/media/show.srt", False),
            ("/media/show.de.ass", False),
            ("/media/forced/show.srt", False),  # forced in dir, not filename
        ],
    )
    def test_filename_patterns(self, path, expected):
        assert is_forced_external_sub(path) == expected

    def test_case_insensitive(self):
        """Regex is case-insensitive."""
        assert is_forced_external_sub("/media/show.FORCED.SRT")
        assert is_forced_external_sub("/media/show.Signs.Ass")


# ---------------------------------------------------------------------------
# classify_forced_result
# ---------------------------------------------------------------------------


class TestClassifyForcedResult:
    def test_provider_foreign_parts_only(self):
        """OpenSubtitles foreign_parts_only -> forced."""
        assert classify_forced_result("test.srt", {"foreign_parts_only": True}) == "forced"

    def test_provider_not_foreign(self):
        """foreign_parts_only=False -> check filename."""
        assert classify_forced_result("test.srt", {"foreign_parts_only": False}) == "full"

    def test_empty_filename(self):
        """Empty filename -> full."""
        assert classify_forced_result("", None) == "full"

    def test_none_provider_data(self):
        """None provider_data -> check filename only."""
        assert classify_forced_result("test.srt", None) == "full"

    @pytest.mark.parametrize(
        "filename,expected",
        [
            ("show.forced.srt", "forced"),
            ("show.foreign.srt", "forced"),
            ("show.signs.srt", "signs"),
            ("show.sign.srt", "signs"),
            ("[Sub] Show - 01 (Signs & Songs).ass", "signs"),
            ("[Sub] Show - 01 (signs only).srt", "signs"),
            ("show.forced.ass", "forced"),
            ("show.en.srt", "full"),
            ("regular_subtitle.srt", "full"),
        ],
    )
    def test_filename_classification(self, filename, expected):
        assert classify_forced_result(filename) == expected

    def test_signs_and_songs_pattern(self):
        """signs & songs pattern -> signs."""
        assert classify_forced_result("[Group] Show - 01 (Signs & Songs).ass") == "signs"

    def test_forced_keyword_in_name(self):
        """'forced' keyword as standalone word -> forced."""
        assert classify_forced_result("show forced eng.srt") == "forced"

    def test_forced_in_underscore_context(self):
        """Underscores separate words: secure_filename turns "English Forced" into this."""
        assert classify_forced_result("show_forced_eng.srt") == "forced"