Repository: Diaoul/subliminal
Pull request #1209 (2025-02-18 22:02:32)

Improve coverage
Improve `MockSubtitle` to allow more testing.
Use `MockSubtitle` to test `score.py`: this allows more flexibility in the tests and makes it independent of any provider.

Core coverage is only missing lines in `core.py`.


Patch:
diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index 6778f5d..6133df3 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -144,6 +144,12 @@ jobs:
           with codecs.open(os.environ["GITHUB_ENV"], mode="a", encoding="utf-8") as file_handler:
               file_handler.write(f"FORCE_COLOR=1\nENV={py}\n")
         shell: python
+      - name: Install other test software (on Linux only)
+        if: matrix.os == 'ubuntu-latest'
+        uses: awalsh128/cache-apt-pkgs-action@v1
+        with:
+          packages: rar
+          version: 1.0
       - name: Setup test environment
         run: |
           hatch -v env create ${ENV}
diff --git a/changelog.d/1125.breaking.rst b/changelog.d/1125.breaking.rst
new file mode 100644
index 0000000..4fd40ba
--- /dev/null
+++ b/changelog.d/1125.breaking.rst
@@ -0,0 +1,1 @@
+Remove the original-encoding CLI option, pass --encoding="" for the same effect.
diff --git a/changelog.d/1209.change.rst b/changelog.d/1209.change.rst
new file mode 100644
index 0000000..65c3299
--- /dev/null
+++ b/changelog.d/1209.change.rst
@@ -0,0 +1,1 @@
+Improve core coverage: score.py, matches.py and archives.py
diff --git a/hatch.toml b/hatch.toml
index 0196eee..d861b72 100644
--- a/hatch.toml
+++ b/hatch.toml
@@ -104,7 +104,8 @@ run-cov-core = [
   "test-cov-core",
   """\
     coverage report --skip-covered --show-missing --fail-under=100 \
-    --omit='src/subliminal/cli.py,src/subliminal/converters/*,src/subliminal/providers/*,src/subliminal/refiners/*' \
+    --omit='src/subliminal/cli.py,src/subliminal/__main__.py,'\
+    'src/subliminal/converters/*,src/subliminal/providers/*,src/subliminal/refiners/*' \
   """,
 ]
 
diff --git a/pyproject.toml b/pyproject.toml
index 83ffd03..8286a6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,7 @@ dependencies = [
 [project.optional-dependencies]
 rar = ["rarfile>=2.7"]
 docs = [
-    "sphinx",
+    "sphinx<8.2",
     "sphinx_rtd_theme>=2",
     "sphinxcontrib-programoutput",
     "sphinx_autodoc_typehints",
@@ -141,7 +141,8 @@ exclude_also = [
     "if TYPE_CHECKING:",
     "@overload",
     "except ImportError",
-    "\\.\\.\\.",
+    "except PackageNotFoundError",
+    "\\.\\.\\.^",
     "raise NotImplementedError()",
     "if __name__ == .__main__.:",
 ]
@@ -149,6 +150,7 @@ show_missing = true
 skip_covered = true
 fail_under = 80
 omit = [
+    "src/subliminal/__main__.py",
     "src/subliminal/cli.py",
 ]
 
diff --git a/src/subliminal/archives.py b/src/subliminal/archives.py
index 21ee5c5..580b2be 100644
--- a/src/subliminal/archives.py
+++ b/src/subliminal/archives.py
@@ -46,14 +46,14 @@ def is_supported_archive(filename: str) -> bool:
     if filename.lower().endswith(ARCHIVE_EXTENSIONS):
         return True
 
-    if filename.lower().endswith('.rar'):
+    if filename.lower().endswith('.rar'):  # pragma: no cover
         msg = 'Install the rarfile module to be able to read rar archives.'
         warnings.warn(msg, UserWarning, stacklevel=2)
 
-    return False
+    return False  # pragma: no cover
 
 
-def scan_archive(path: str | os.PathLike, name: str | None = None) -> Video:  # pragma: no cover
+def scan_archive(path: str | os.PathLike, name: str | None = None) -> Video:
     """Scan an archive from a `path`.
 
     :param str path: existing path to the archive.
@@ -78,7 +78,7 @@ def scan_archive(path: str | os.PathLike, name: str | None = None) -> Video:  #
     raise ArchiveError(msg)
 
 
-def scan_archive_rar(path: str | os.PathLike, name: str | None = None) -> Video:  # pragma: no cover
+def scan_archive_rar(path: str | os.PathLike, name: str | None = None) -> Video:
     """Scan a rar archive from a `path`.
 
     :param str path: existing path to the archive.
@@ -90,7 +90,7 @@ def scan_archive_rar(path: str | os.PathLike, name: str | None = None) -> Video:
     path = os.fspath(path)
     # check for non-existing path
     if not os.path.exists(path):  # pragma: no cover
-        msg = 'Path does not exist'
+        msg = f'Path does not exist: {path!r}'
         raise ValueError(msg)
 
     if not is_rarfile(path):
diff --git a/src/subliminal/cli.py b/src/subliminal/cli.py
index 654ffb1..55c6351 100644
--- a/src/subliminal/cli.py
+++ b/src/subliminal/cli.py
@@ -449,13 +449,7 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None:
     type=click.STRING,
     metavar='ENC',
     default='utf-8',
-    help='Force subtitle file encoding, default is utf-8.',
-)
-@click.option(
-    '--original-encoding',
-    is_flag=True,
-    default=False,
-    help='Preserve original subtitle file encoding.',
+    help='Force subtitle file encoding, set to an empty string to preserve the original encoding. Default is utf-8.',
 )
 @click.option(
     '-s',
@@ -557,7 +551,6 @@ def download(
     use_ctime: bool,
     directory: str | None,
     encoding: str | None,
-    original_encoding: bool,
     single: bool,
     force: bool,
     hearing_impaired: tuple[bool | None, ...],
@@ -582,12 +575,9 @@ def download(
     # process parameters
     language_set = set(language)
 
-    # preserve original encoding
-    if original_encoding:
+    # no encoding specified, default to None
+    if not encoding:
         encoding = None
-    # no encoding specified, default to utf-8
-    elif encoding is None:
-        encoding = 'utf-8'
 
     # language_type
     hearing_impaired_flag: bool | None = None
diff --git a/src/subliminal/providers/mock.py b/src/subliminal/providers/mock.py
index bed5334..5f418b6 100644
--- a/src/subliminal/providers/mock.py
+++ b/src/subliminal/providers/mock.py
@@ -8,8 +8,9 @@ from itertools import count
 from typing import TYPE_CHECKING, Any, ClassVar
 
 from babelfish import LANGUAGES, Language  # type: ignore[import-untyped]
+from guessit import guessit  # type: ignore[import-untyped]
 
-from subliminal.exceptions import NotInitializedProviderError
+from subliminal.exceptions import NotInitializedProviderError, ProviderError
 from subliminal.matches import guess_matches
 from subliminal.subtitle import Subtitle
 from subliminal.video import Episode, Movie, Video
@@ -25,13 +26,25 @@ logger = logging.getLogger(__name__)
 class MockSubtitle(Subtitle):
     """Mock Subtitle."""
 
-    provider_name: ClassVar[str] = 'mock'
     _ids: ClassVar = count(0)
 
+    #: Provider name, modify in subclasses
+    provider_name: ClassVar[str] = 'mock'
+
+    #: Fake content, will be copied to 'content' with :meth:`MockProvider.download_subtitle`
     fake_content: bytes
+
+    #: Video name to match to be listed by :meth:`MockProvider.list_subtitles`
     video_name: str
+
+    #: A set of matches to add to the set when :meth:`get_matches` is called
     matches: set[str]
-    force_matches: bool
+
+    #: Guesses used as argument to compute the matches with :func:`guess_matches`
+    parameters: dict[str, Any]
+
+    #: Release name to be parsed with guessit and added to the :attr:`parameters`
+    release_name: str
 
     def __init__(
         self,
@@ -41,7 +54,8 @@ class MockSubtitle(Subtitle):
         fake_content: bytes = b'',
         video_name: str = '',
         matches: Set[str] | None = None,
-        parameters: dict[str, Any] | None = None,
+        parameters: Mapping[str, Any] | None = None,
+        release_name: str = '',
         **kwargs: Any,
     ) -> None:
         # generate unique id for mock subtitle
@@ -55,15 +69,22 @@ class MockSubtitle(Subtitle):
         )
         self.fake_content = fake_content
         self.video_name = video_name
-        self.force_matches = matches is not None
         self.matches = set(matches) if matches is not None else set()
         self.parameters = dict(parameters) if parameters is not None else {}
+        self.release_name = release_name
 
     def get_matches(self, video: Video) -> set[str]:
         """Get the matches against the `video`."""
-        if self.force_matches:
-            return self.matches
-        return guess_matches(video, self.parameters)
+        # Use the parameters as guesses
+        matches = guess_matches(video, self.parameters)
+
+        # Parse the release_name and guess more matches
+        if self.release_name:
+            video_type = 'episode' if isinstance(video, Episode) else 'movie'
+            matches |= guess_matches(video, guessit(self.release_name, {'type': video_type}))
+
+        # Force add more matches
+        return matches | self.matches
 
 
 class MockProvider(Provider):
@@ -77,22 +98,27 @@ class MockProvider(Provider):
 
     logged_in: bool
     subtitle_pool: list[MockSubtitle]
+    is_broken: bool
 
     def __init__(self, subtitle_pool: Sequence[MockSubtitle] | None = None) -> None:
         self.logged_in = False
         self.subtitle_pool = list(self.internal_subtitle_pool)
         if subtitle_pool is not None:
             self.subtitle_pool.extend(list(subtitle_pool))
+        self.is_broken = False
 
     def initialize(self) -> None:
         """Initialize the provider."""
+        logger.info('Mock provider %s was initialized', self.__class__.__name__)
         self.logged_in = True
 
     def terminate(self) -> None:
         """Terminate the provider."""
         if not self.logged_in:
+            logger.info('Mock provider %s was not terminated', self.__class__.__name__)
             raise NotInitializedProviderError
 
+        logger.info('Mock provider %s was terminated', self.__class__.__name__)
         self.logged_in = False
 
     def query(
@@ -102,27 +128,66 @@ class MockProvider(Provider):
         matches: Set[str] | None = None,
     ) -> list[MockSubtitle]:
         """Query the provider for subtitles."""
+        if self.is_broken:
+            msg = f'Mock provider {self.__class__.__name__} query raised an error'
+            raise ProviderError(msg)
+
         subtitles = []
         for lang in languages:
-            subtitle = MockSubtitle(language=lang, video=video, matches=matches)
+            subtitle = self.subtitle_class(language=lang, video=video, matches=matches)
             subtitles.append(subtitle)
+        logger.info(
+            'Mock provider %s query for video %r and languages %s: %d',
+            self.__class__.__name__,
+            video.name if video else None,
+            languages,
+            len(subtitles),
+        )
         return subtitles
 
     def list_subtitles(self, video: Video, languages: Set[Language]) -> list[MockSubtitle]:
         """List all the subtitles for the video."""
-        return [
+        if self.is_broken:
+            msg = f'Mock provider {self.__class__.__name__} list_subtitles raised an error'
+            raise ProviderError(msg)
+
+        subtitles = [
             subtitle
             for subtitle in self.subtitle_pool
             if subtitle.language in languages and subtitle.video_name == video.name
         ]
+        logger.info(
+            'Mock provider %s list subtitles for video %r and languages %s: %d',
+            self.__class__.__name__,
+            video.name,
+            languages,
+            len(subtitles),
+        )
+        return subtitles
 
     def download_subtitle(self, subtitle: MockSubtitle) -> None:
         """Download the content of the subtitle."""
+        if self.is_broken:
+            msg = f'Mock provider {self.__class__.__name__} download_subtitle raised an error'
+            raise ProviderError(msg)
+
+        logger.info(
+            'Mock provider %s download subtitle %s',
+            self.__class__.__name__,
+            subtitle,
+        )
         subtitle.content = subtitle.fake_content
 
 
-def mock_subtitle_provider(name: str, subtitles_info: Sequence[Mapping[str, Any]]) -> str:
+def mock_subtitle_provider(
+    name: str,
+    subtitles_info: Sequence[Mapping[str, Any]],
+    languages: Set[Language] | None = None,
+    video_types: tuple[type[Episode] | type[Movie], ...] = (Episode, Movie),
+) -> str:
     """Mock a subtitle provider, providing subtitles."""
+    languages = set(languages) if languages else {Language(lang) for lang in LANGUAGES}
+
     name_lower = name.lower()
     subtitle_class_name = f'{name}Subtitle'
     provider_class_name = f'{name}Provider'
@@ -138,6 +203,8 @@ def mock_subtitle_provider(name: str, subtitles_info: Sequence[Mapping[str, Any]
         {
             'subtitle_class': MyMockSubtitle,
             'internal_subtitle_pool': subtitle_pool,
+            'languages': languages,
+            'video_types': video_types,
         },
     )
 
diff --git a/src/subliminal/score.py b/src/subliminal/score.py
index c691e4c..585e259 100644
--- a/src/subliminal/score.py
+++ b/src/subliminal/score.py
@@ -134,7 +134,7 @@ def get_scores(video: Video) -> dict[str, Any]:
 
 def match_hearing_impaired(subtitle: Subtitle, *, hearing_impaired: bool | None = None) -> bool:
     """Match hearing impaired, if it is defined for the subtitle."""
-    return (
+    return (  # pragma: no cover
         hearing_impaired is not None
         and subtitle.hearing_impaired is not None
         and subtitle.hearing_impaired == hearing_impaired
@@ -181,16 +181,25 @@ def compute_score(subtitle: Subtitle, video: Video, **kwargs: Any) -> int:
         if 'imdb_id' in matches:
             logger.debug('Adding imdb_id match equivalents')
             matches |= {'series', 'year', 'country', 'season', 'episode'}
-        if 'tvdb_id' in matches:
-            logger.debug('Adding tvdb_id match equivalents')
+        if 'series_tmdb_id' in matches:
+            logger.debug('Adding series_tmdb_id match equivalents')
+            matches |= {'series', 'year', 'country'}
+        if 'tmdb_id' in matches:
+            logger.debug('Adding tmdb_id match equivalents')
             matches |= {'series', 'year', 'country', 'season', 'episode'}
         if 'series_tvdb_id' in matches:
             logger.debug('Adding series_tvdb_id match equivalents')
             matches |= {'series', 'year', 'country'}
+        if 'tvdb_id' in matches:
+            logger.debug('Adding tvdb_id match equivalents')
+            matches |= {'series', 'year', 'country', 'season', 'episode'}
     elif isinstance(video, Movie):  # pragma: no branch
         if 'imdb_id' in matches:
             logger.debug('Adding imdb_id match equivalents')
             matches |= {'title', 'year', 'country'}
+        if 'tmdb_id' in matches:
+            logger.debug('Adding tmdb_id match equivalents')
+            matches |= {'title', 'year', 'country'}
 
     # compute the score
     score = int(sum(scores.get(match, 0) for match in matches))
diff --git a/src/subliminal/utils.py b/src/subliminal/utils.py
index 3280662..5961496 100644
--- a/src/subliminal/utils.py
+++ b/src/subliminal/utils.py
@@ -219,7 +219,7 @@ def creation_date(filepath: os.PathLike | str) -> float:
     See https://stackoverflow.com/a/39501288/1709587 for explanation.
     """
     # Use creation time (although it may not be correct)
-    if platform.system() == 'Windows':
+    if platform.system() == 'Windows':  # pragma: no cover
         return os.path.getctime(filepath)
     stat = os.stat(filepath)
     try:
@@ -366,11 +366,11 @@ def clip(value: float, minimum: float | None, maximum: float | None) -> float:
 
 def split_doc_args(args: str | None) -> list[str]:
     """Split the arguments of a docstring (in Sphinx docstyle)."""
-    if not args:
+    if not args:  # pragma: no cover
         return []
     split_regex = re.compile(r'(?m):((param|type)\s|(return|yield|raise|rtype|ytype)s?:)')
     split_indices = [m.start() for m in split_regex.finditer(args)]
-    if len(split_indices) == 0:
+    if len(split_indices) == 0:  # pragma: no cover
         return []
     next_indices = [*split_indices[1:], None]
     parts = [args[i:j].strip() for i, j in zip(split_indices, next_indices)]
@@ -388,10 +388,10 @@ def get_argument_doc(fun: Callable) -> dict[str, str]:
     ret = {}
     for p in parts:
         m = param_regex.match(p)
-        if not m:
+        if not m:  # pragma: no cover
             continue
         _, name, desc = m.groups()
-        if name is None:
+        if name is None:  # pragma: no cover
             continue
         ret[name] = ' '.join(desc.strip().split())
 
diff --git a/tox.ini b/tox.ini
index c629f9f..6eff0a8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -67,7 +67,8 @@ commands =
         -m core --cov=subliminal --cov-report= --cov-fail-under=0 \
         {posargs:-n auto}
     coverage report \
-        --omit='subliminal/cli.py,subliminal/converters/*,subliminal/providers/*,subliminal/refiners/*' \
+        --omit='src/subliminal/cli.py,src/subliminal/__main__.py,'\
+            'src/subliminal/converters/*,src/subliminal/providers/*,src/subliminal/refiners/*' \
         --skip-covered --show-missing --fail-under=100
 
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5181d9c..a054b33 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,10 +8,11 @@ from zipfile import ZipFile
 
 import pytest
 import requests
-from babelfish import Country  # type: ignore[import-untyped]
+from babelfish import Country, Language  # type: ignore[import-untyped]
 
 from subliminal import Episode, Movie
 from subliminal.cache import region
+from subliminal.providers.mock import MockSubtitle
 
 TESTS = os.path.dirname(__file__)
 
@@ -478,6 +479,75 @@ def episodes() -> dict[str, Episode]:
     }
 
 
+@pytest.fixture
+def subtitles() -> dict[str, MockSubtitle]:
+    return {
+        'man_of_steel==empty': MockSubtitle(
+            language=Language('eng'),
+            subtitle_id='man_of_steel==empty',
+            hearing_impaired=True,
+            page_link=None,
+            encoding='utf-8',
+        ),
+        'bbt_s07e05==empty': MockSubtitle(
+            language=Language('eng'),
+            subtitle_id='bbt_s07e05==empty',
+            foreign_only=False,
+            page_link=None,
+        ),
+        'bbt_s07e05==series_year_country': MockSubtitle(
+            language=Language('eng'),
+            subtitle_id='bbt_s07e05==series_year_country',
+            hearing_impaired=False,
+            page_link=None,
+            parameters={
+                'title': 'the big BANG theory',
+                'season': 6,
+                'episode': 4,
+                'episode_title': None,
+                'year': None,
+                'release_group': '1080p',
+            },
+        ),
+        'man_of_steel==hash': MockSubtitle(
+            language=Language('eng'),
+            subtitle_id='man_of_steel==hash',
+            foreign_only=True,
+            page_link=None,
+            encoding='utf-8',
+            matches={'hash'},
+        ),
+        'man_of_steel==imdb_id': MockSubtitle(
+            language=Language('eng'),
+            subtitle_id='man_of_steel==imdb_id',
+            page_link=None,
+            encoding='utf-8',
+            matches={'imdb_id', 'country'},
+            parameters={
+                'title': 'Man of Steel',
+                'year': 2013,
+                'imdb_id': 'tt770828',
+                'season': None,
+                'episode': None,
+            },
+            release_name='man.of.steel.2013.720p.bluray.x264-felony.mkv',
+        ),
+        'bbt_s07e05==episode_title': MockSubtitle(
+            language=Language('pol'),
+            subtitle_id='bbt_s07e05==episode_title',
+            page_link=None,
+            encoding='utf-8',
+            parameters={
+                'title': None,
+                'season': 7,
+                'episode': 5,
+                'year': None,
+            },
+            release_name='The.Big.Bang.Theory.S07E05.The.Workplace.Proximity.720p.HDTV.x264-DIMENSION.mkv',
+        ),
+    }
+
+
 @pytest.fixture(scope='session')
 def mkv():
     data_path = os.path.join(TESTS, 'data', 'mkv')
@@ -535,7 +605,7 @@ def rar(mkv):
         existing_videos = [v for v in videos if v and os.path.isfile(v)]
         filename = os.path.join(data_path, name + '.rar')
         if not os.path.exists(filename):
-            subprocess.run(['rar', 'a', filename, *existing_videos], shell=True)
+            subprocess.run(['rar', 'a', '-ep', filename, *existing_videos], check=True)
         if os.path.exists(filename):
             files[name] = filename
 
diff --git a/tests/test_archives.py b/tests/test_archives.py
new file mode 100644
index 0000000..0475faf
--- /dev/null
+++ b/tests/test_archives.py
@@ -0,0 +1,74 @@
+# ruff: noqa: PT011, SIM115
+from __future__ import annotations
+
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+from subliminal.archives import is_supported_archive, scan_archive, scan_archive_rar
+from subliminal.exceptions import ArchiveError
+
+unix_platform = pytest.mark.skipif(
+    not sys.platform.startswith('linux'),
+    reason='only on linux platform',
+)
+
+# Core test
+pytestmark = [pytest.mark.core, unix_platform]
+
+
+def test_is_supported_archive(rar: dict[str, str], mkv: dict[str, str]) -> None:
+    assert is_supported_archive(rar['simple'])
+    assert not is_supported_archive(mkv['test1'])
+
+
+def test_scan_archive_with_one_video(rar: dict[str, str], mkv: dict[str, str]) -> None:
+    if 'video' not in rar:
+        return
+    rar_file = rar['video']
+    actual = scan_archive(rar_file)
+
+    expected = os.fspath(Path(rar_file).parent / Path(mkv['test1']).name)
+    assert actual.name == expected
+
+
+def test_scan_archive_with_multiple_videos(rar: dict[str, str], mkv: dict[str, str]) -> None:
+    if 'videos' not in rar:
+        return
+    rar_file = rar['videos']
+    actual = scan_archive(rar_file)
+
+    expected = os.fspath(Path(rar_file).parent / Path(mkv['test5']).name)
+    assert actual.name == expected
+
+
+def test_scan_archive_with_no_video(rar: dict[str, str]) -> None:
+    with pytest.raises(ArchiveError) as excinfo:
+        scan_archive(rar['simple'])
+    assert excinfo.value.args == ('No video in archive',)
+
+
+def test_scan_bad_archive(mkv: dict[str, str]) -> None:
+    with pytest.raises(ArchiveError) as excinfo:
+        scan_archive(mkv['test1'])
+    assert excinfo.value.args == ("'.mkv' is not a valid archive",)
+
+
+def test_scan_rar_not_a_file(mkv: dict[str, str]) -> None:
+    with pytest.raises(ValueError) as excinfo:
+        scan_archive_rar('not_a_file.txt')
+    assert excinfo.value.args == ("Path does not exist: 'not_a_file.txt'",)
+
+
+def test_scan_rar_bad_archive(mkv: dict[str, str]) -> None:
+    with pytest.raises(ValueError) as excinfo:
+        scan_archive_rar(mkv['test1'])
+    assert excinfo.value.args == ("'.mkv' is not a valid archive",)
+
+
+def test_scan_password_protected_archive(rar: dict[str, str]) -> None:
+    with pytest.raises(ArchiveError) as excinfo:
+        scan_archive(rar['pwd-protected'])
+    assert excinfo.value.args == ('Rar requires a password',)
diff --git a/tests/test_core.py b/tests/test_core.py
index 9ffce7b..158e0f0 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -2,7 +2,6 @@
 from __future__ import annotations
 
 import os
-import sys
 from datetime import datetime, timedelta, timezone
 from typing import Any
 from unittest.mock import Mock
@@ -27,11 +26,6 @@ from subliminal.video import Episode, Movie
 # Core test
 pytestmark = pytest.mark.core
 
-unix_platform = pytest.mark.skipif(
-    not sys.platform.startswith('linux'),
-    reason='only on linux platform',
-)
-
 
 def test_check_video_languages(movies: dict[str, Movie]) -> None:
     video = movies['man_of_steel']
@@ -419,44 +413,3 @@ def test_save_subtitles_single_directory_encoding(movies: dict[str, Movie], tmpd
     path = os.path.join(str(tmpdir), os.path.splitext(os.path.split(movies['man_of_steel'].name)[1])[0] + '.srt')
     assert os.path.exists(path)
     assert open(path, encoding='utf-8').read() == 'ハローワールド'
-
-
-@unix_platform
-def test_scan_archive_with_one_video(rar: dict[str, str], mkv: dict[str, str]) -> None:
-    if 'video' not in rar:
-        return
-    rar_file = rar['video']
-    actual = scan_archive(rar_file)
-
-    assert actual.name == os.path.join(os.path.split(rar_file)[0], mkv['test1'])
-
-
-@unix_platform
-def test_scan_archive_with_multiple_videos(rar: dict[str, str], mkv: dict[str, str]) -> None:
-    if 'video' not in rar:
-        return
-    rar_file = rar['videos']
-    actual = scan_archive(rar_file)
-
-    assert actual.name == os.path.join(os.path.split(rar_file)[0], mkv['test5'])
-
-
-@unix_platform
-def test_scan_archive_with_no_video(rar: dict[str, str]) -> None:
-    with pytest.raises(ArchiveError) as excinfo:
-        scan_archive(rar['simple'])
-    assert excinfo.value.args == ('No video in archive',)
-
-
-@unix_platform
-def test_scan_bad_archive(mkv: dict[str, str]) -> None:
-    with pytest.raises(ArchiveError) as excinfo:
-        scan_archive(mkv['test1'])
-    assert excinfo.value.args == ("'.mkv' is not a valid archive",)
-
-
-@unix_platform
-def test_scan_password_protected_archive(rar: dict[str, str]) -> None:
-    with pytest.raises(ArchiveError) as excinfo:
-        scan_archive(rar['pwd-protected'])
-    assert excinfo.value.args == ('Rar requires a password',)
diff --git a/tests/test_score.py b/tests/test_score.py
index 115dc1c..eb82418 100644
--- a/tests/test_score.py
+++ b/tests/test_score.py
@@ -1,18 +1,14 @@
 from __future__ import annotations
 
 import pytest
-from babelfish import Language  # type: ignore[import-untyped]
 
-from subliminal.providers.addic7ed import Addic7edSubtitle
-from subliminal.providers.opensubtitles import OpenSubtitlesSubtitle
-from subliminal.providers.podnapisi import PodnapisiSubtitle
 from subliminal.score import compute_score, episode_scores, movie_scores, solve_episode_equations, solve_movie_equations
 
 # Core test
 pytestmark = pytest.mark.core
 
 
-def test_episode_equations():
+def test_episode_equations() -> None:
     expected_scores = {}
     for symbol, score in solve_episode_equations().items():
         expected_scores[str(symbol)] = score
@@ -20,7 +16,7 @@ def test_episode_equations():
     assert episode_scores == expected_scores
 
 
-def test_movie_equations():
+def test_movie_equations() -> None:
     expected_scores = {}
     for symbol, score in solve_movie_equations().items():
         expected_scores[str(symbol)] = score
@@ -28,85 +24,38 @@ def test_movie_equations():
     assert movie_scores == expected_scores
 
 
-def test_compute_score(episodes):
+def test_compute_score(episodes, subtitles) -> None:
     video = episodes['bbt_s07e05']
-    subtitle = Addic7edSubtitle(
-        language=Language('eng'),
-        subtitle_id='',
-        hearing_impaired=True,
-        page_link=None,
-        series='the big BANG theory',
-        season=6,
-        episode=4,
-        title=None,
-        year=None,
-        release_group='1080p',
-    )
+    subtitle = subtitles['bbt_s07e05==series_year_country']
     expected_score = episode_scores['series'] + episode_scores['year'] + episode_scores['country']
     assert compute_score(subtitle, video) == expected_score
 
 
-def test_get_score_cap(movies):
+def test_get_score_cap(movies, subtitles) -> None:
     video = movies['man_of_steel']
-    subtitle = OpenSubtitlesSubtitle(
-        language=Language('eng'),
-        subtitle_id='1',
-        hearing_impaired=True,
-        page_link=None,
-        matched_by='hash',
-        movie_kind='movie',
-        moviehash='5b8f8f4e41ccb21e',
-        movie_name='Man of Steel',
-        movie_release_name='man.of.steel.2013.720p.bluray.x264-felony.mkv',
-        movie_year=2013,
-        movie_imdb_id='tt770828',
-        series_season=None,
-        series_episode=None,
-        filename='',
-        encoding='utf-8',
-    )
-    assert compute_score(subtitle, video) == movie_scores['hash']
+    subtitle = subtitles['man_of_steel==hash']
+
+    expected_matches = {'hash', 'country'}
+    assert subtitle.get_matches(video) == expected_matches
+
+    expected = movie_scores['hash']
+    assert compute_score(subtitle, video) == expected
 
 
-def test_compute_score_movie_imdb_id(movies):
+def test_compute_score_movie_imdb_id(movies, subtitles) -> None:
     video = movies['man_of_steel']
-    subtitle = OpenSubtitlesSubtitle(
-        language=Language('eng'),
-        subtitle_id='1',
-        hearing_impaired=True,
-        page_link=None,
-        matched_by='hash',
-        movie_kind='movie',
-        moviehash=None,
-        movie_name='Man of Steel',
-        movie_release_name='man.of.steel.2013.720p.bluray.x264-felony.mkv',
-        movie_year=2013,
-        movie_imdb_id='tt770828',
-        series_season=None,
-        series_episode=None,
-        filename='',
-        encoding='utf-8',
-    )
-    assert compute_score(subtitle, video) == sum(
+    subtitle = subtitles['man_of_steel==imdb_id']
+    expected = sum(
         movie_scores.get(m, 0)
         for m in ('imdb_id', 'title', 'year', 'country', 'release_group', 'source', 'resolution', 'video_codec')
     )
+    assert compute_score(subtitle, video) == expected
 
 
-def test_compute_score_episode_title(episodes):
+def test_compute_score_episode_title(episodes, subtitles) -> None:
     video = episodes['bbt_s07e05']
-    subtitle = PodnapisiSubtitle(
-        language=Language('eng'),
-        subtitle_id='1',
-        hearing_impaired=True,
-        page_link=None,
-        releases=['The.Big.Bang.Theory.S07E05.The.Workplace.Proximity.720p.HDTV.x264-DIMENSION.mkv'],
-        title=None,
-        season=7,
-        episode=5,
-        year=None,
-    )
-    assert compute_score(subtitle, video) == sum(
+    subtitle = subtitles['bbt_s07e05==episode_title']
+    expected = sum(
         episode_scores.get(m, 0)
         for m in (
             'series',
@@ -121,25 +70,34 @@ def test_compute_score_episode_title(episodes):
             'title',
         )
     )
+    assert compute_score(subtitle, video) == expected
+
+
+@pytest.mark.parametrize('id_type', ['imdb_id', 'tmdb_id', 'tvdb_id'])
+def test_compute_score_episode_imdb_id_only(episodes, subtitles, id_type: str) -> None:
+    video = episodes['bbt_s07e05']
+    subtitle = subtitles['bbt_s07e05==empty']
+    subtitle.matches = {id_type}
+
+    expected = sum(episode_scores.get(m, 0) for m in ('series', 'year', 'country', 'season', 'episode'))
+    assert compute_score(subtitle, video) == expected
+
+
+@pytest.mark.parametrize('id_type', ['series_imdb_id', 'series_tmdb_id', 'series_tvdb_id'])
+def test_compute_score_episode_series_imdb_id_only(episodes, subtitles, id_type: str) -> None:
+    video = episodes['bbt_s07e05']
+    subtitle = subtitles['bbt_s07e05==empty']
+    subtitle.matches = {id_type}
+
+    expected = sum(episode_scores.get(m, 0) for m in ('series', 'year', 'country'))
+    assert compute_score(subtitle, video) == expected
 
 
-def test_compute_score_hash_hearing_impaired(movies):
+@pytest.mark.parametrize('id_type', ['imdb_id', 'tmdb_id'])
+def test_compute_score_movie_imdb_id_only(movies, subtitles, id_type: str) -> None:
     video = movies['man_of_steel']
-    subtitle = OpenSubtitlesSubtitle(
-        language=Language('eng'),
-        subtitle_id='1',
-        hearing_impaired=True,
-        page_link=None,
-        matched_by='hash',
-        movie_kind='movie',
-        moviehash='5b8f8f4e41ccb21e',
-        movie_name='Man of Steel',
-        movie_release_name='man.of.steel.2013.720p.bluray.x264-felony.mkv',
-        movie_year=2013,
-        movie_imdb_id='tt770828',
-        series_season=None,
-        series_episode=None,
-        filename='',
-        encoding='utf-8',
-    )
-    assert compute_score(subtitle, video, hearing_impaired=True) == movie_scores['hash']
+    subtitle = subtitles['man_of_steel==empty']
+    subtitle.matches = {id_type}
+
+    expected = sum(movie_scores.get(m, 0) for m in ('title', 'year', 'country'))
+    assert compute_score(subtitle, video) == expected
diff --git a/tests/test_subtitle.py b/tests/test_subtitle.py
index 551a7fd..5918996 100644
--- a/tests/test_subtitle.py
+++ b/tests/test_subtitle.py
@@ -239,6 +239,7 @@ def test_subtitle_invalid_encoding():
         encoding='rubbish',
     )
     assert subtitle.encoding is None
+    assert subtitle.hearing_impaired is False
 
 
 def test_subtitle_guess_encoding_utf8():
@@ -295,6 +296,17 @@ def test_subtitle_info(monkeypatch) -> None:
     assert isinstance(subtitle.info, str)
 
 
+def test_embedded_subtitle_info(monkeypatch) -> None:
+    subtitle = EmbeddedSubtitle(
+        Language('ita'),
+    )
+    text = '1\n00:00:20,000 --> 00:00:24,400\nIn risposta alla sua lettera del\n\n'
+    monkeypatch.setattr(Subtitle, 'text', text)
+    assert subtitle.is_valid() is True
+    assert isinstance(subtitle.id, str)
+    assert isinstance(subtitle.info, str)
+
+
 def test_embedded_subtitle_info_hearing_impaired(monkeypatch) -> None:
     subtitle = EmbeddedSubtitle(
         Language('spa'),
@@ -303,6 +315,7 @@ def test_embedded_subtitle_info_hearing_impaired(monkeypatch) -> None:
     text = '1\n00:00:20,000 --> 00:00:24,400\nEn respuesta a su carta de\n\n'
     monkeypatch.setattr(Subtitle, 'text', text)
     assert subtitle.is_valid() is True
+    assert subtitle.hearing_impaired is True
     assert isinstance(subtitle.id, str)
     assert isinstance(subtitle.info, str)
 
@@ -315,5 +328,6 @@ def test_embedded_subtitle_info_foreign_only(monkeypatch) -> None:
     text = '1\n00:00:20,000 --> 00:00:24,400\nEn réponse à votre honorée du tant\n\n'
     monkeypatch.setattr(Subtitle, 'text', text)
     assert subtitle.is_valid() is True
+    assert subtitle.foreign_only is True
     assert isinstance(subtitle.id, str)
     assert isinstance(subtitle.info, str)