1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
from __future__ import annotations
from typing import TYPE_CHECKING, TypeAlias
import pytest
from streamlink.plugin.plugin import Plugin
if TYPE_CHECKING:
from collections.abc import Sequence
from re import Match
from streamlink.plugin.plugin import Matcher
TUrl: TypeAlias = str
TName: TypeAlias = str
TUrlNamed: TypeAlias = tuple[TName, TUrl]
TUrlOrNamedUrl: TypeAlias = TUrl | TUrlNamed
TMatchGroup: TypeAlias = dict[str, str] | Sequence[str | None]
generic_negative_matches = [
"http://example.com/",
"https://example.com/",
"https://example.com/index.html",
]
_plugin_can_handle_url_classnames: set[str] = set()
class PluginCanHandleUrl:
__plugin__: type[Plugin]
should_match: list[TUrlOrNamedUrl] = []
"""
A list of URLs that should match any of the plugin's URL regexes.
URL can be a tuple of a matcher name and the URL itself.
Example:
should_match = [
"https://foo",
("bar", "https://bar"),
]
"""
should_match_groups: list[tuple[TUrlOrNamedUrl, TMatchGroup]] = []
"""
A list of URL+capturegroup tuples, where capturegroup can be a dict (re.Match.groupdict()) or a tuple (re.Match.groups()).
URL can be a tuple of a matcher name and the URL itself.
URLs defined in this list automatically get appended to the should_match list.
Values in capturegroup dictionaries that are None get ignored when comparing and must be omitted in the test fixtures.
Example:
[
("https://foo", {"foo": "foo"}),
("https://bar", ("bar", None)),
("https://bar/baz", ("bar", "baz")),
(("qux", "https://qux"), {"qux": "qux"}),
]
"""
should_not_match: list[TUrl] = []
"""
A list of URLs that should not match any of the plugin's URL regexes.
Generic negative URL matches are appended to this list automatically and must not be defined.
Example:
[
"https://foo",
]
"""
# ---- test utils
@classmethod
def matchers(cls) -> list[Matcher]:
empty: list[Matcher] = []
return cls.__plugin__.matchers or empty
@classmethod
def urls_all(cls) -> list[TUrlOrNamedUrl]:
return cls.should_match + [item for item, groups in cls.should_match_groups]
@classmethod
def urls_unnamed(cls) -> list[TUrl]:
return [item for item in cls.urls_all() if type(item) is str]
@classmethod
def urls_named(cls) -> list[TUrlNamed]:
return [item for item in cls.urls_all() if type(item) is tuple]
@classmethod
def urlgroups_unnamed(cls) -> list[tuple[TUrl, TMatchGroup]]:
return [(item, groups) for item, groups in cls.should_match_groups if type(item) is str]
@classmethod
def urlgroups_named(cls) -> list[tuple[TName, TUrl, TMatchGroup]]:
return [(item[0], item[1], groups) for item, groups in cls.should_match_groups if type(item) is tuple]
@classmethod
def urls_negative(cls) -> list[TUrl]:
return cls.should_not_match + generic_negative_matches
@staticmethod
def _get_match_groups(match: Match, grouptype: type[TMatchGroup]) -> TMatchGroup:
return (
# ignore None values in capture group dicts
{k: v for k, v in match.groupdict().items() if v is not None}
if grouptype is dict
# capture group tuples
else match.groups()
)
# ---- misc fixtures
@pytest.fixture()
def classnames(self):
yield _plugin_can_handle_url_classnames
_plugin_can_handle_url_classnames.add(self.__class__.__name__)
# ---- tests
def test_class_setup(self):
assert hasattr(self, "__plugin__"), "Test has a __plugin__ attribute"
assert issubclass(self.__plugin__, Plugin), "Test has a __plugin__ that is a subclass of the Plugin class"
assert self.should_match or self.should_match_groups, "Test has at least one positive URL"
def test_class_name(self, classnames: set[str]):
assert self.__class__.__name__ not in classnames
# ---- all tests below are parametrized dynamically via conftest.py
def test_all_matchers_match(self, matcher: Matcher):
assert any( # pragma: no branch
matcher.pattern.match(url)
for url in [(item if type(item) is str else item[1]) for item in self.urls_all()]
), "Matcher matches at least one URL" # fmt: skip
def test_all_named_matchers_have_tests(self, matcher: Matcher):
assert any( # pragma: no branch
name == matcher.name
for name, url in self.urls_named()
), "Named matcher does have a test" # fmt: skip
def test_url_matches_positive_unnamed(self, url: TUrl):
assert any( # pragma: no branch
matcher.pattern.match(url)
for matcher in self.matchers()
), "Unnamed URL test matches at least one unnamed matcher" # fmt: skip
def test_url_matches_positive_named(self, name: TName, url: TUrl):
assert [ # pragma: no branch
matcher.name
for matcher in self.matchers()
if matcher.pattern.match(url)
] == [name], "Named URL test exactly matches one named matcher" # fmt: skip
def test_url_matches_groups_unnamed(self, url: TUrl, groups: TMatchGroup):
matches = [matcher.pattern.match(url) for matcher in self.matchers() if matcher.name is None]
match = next((match for match in matches if match), None) # pragma: no branch
result = None if not match else self._get_match_groups(match, type(groups))
assert result == groups, "URL capture groups match the results of the first matching unnamed matcher"
def test_url_matches_groups_named(self, name: TName, url: TUrl, groups: TMatchGroup):
matches = [(matcher.name, matcher.pattern.match(url)) for matcher in self.matchers() if matcher.name is not None]
mname, match = next(((mname, match) for mname, match in matches if match), (None, None)) # pragma: no branch
result = None if not match else self._get_match_groups(match, type(groups))
assert (mname, result) == (name, groups), "URL capture groups match the results of the matching named matcher"
def test_url_matches_negative(self, url: TUrl):
assert not any( # pragma: no branch
matcher.pattern.match(url)
for matcher in self.matchers()
), "URL does not match any matcher" # fmt: skip
|