mirror of https://github.com/streamlink/streamlink
tests.plugins: rewrite PluginCanHandleUrl tests
- Rewrite dynamic test parametrization - Test named plugin matchers - Update tests with named matchers accordingly - Fix incorrect test class names
This commit is contained in:
parent
e629975e71
commit
66244e32d7
|
@ -223,6 +223,11 @@ Adding plugins
|
|||
capture group names and values (excluding ``None`` values), or a tuple of unnamed capture group values. URLs from the
|
||||
``should_match_groups`` list automatically get added to ``should_match`` and don't need to be added twice.
|
||||
|
||||
If the plugin defines named matchers, then URLs in the test fixtures must be tuples of the matcher name and the URL itself.
|
||||
Unnamed matchers must not match named URL test fixtures and vice versa.
|
||||
|
||||
Every plugin matcher must have at least one URL test fixture that matches.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from streamlink.plugins.pluginfile import MyPluginClassName
|
||||
|
@ -234,14 +239,15 @@ Adding plugins
|
|||
|
||||
should_match = [
|
||||
"https://host/path/one",
|
||||
"https://host/path/two",
|
||||
("specific-path-matcher", "https://host/path/two"),
|
||||
]
|
||||
|
||||
should_match_groups = [
|
||||
("https://host/stream/123", {"stream": "123"}),
|
||||
("https://host/user/one", {"user": "one"}),
|
||||
("https://host/stream/456", ("456", None)),
|
||||
("https://host/user/two", (None, "two")),
|
||||
("https://host/stream/456/foo", ("456", "foo")),
|
||||
(("user-matcher", "https://host/user/one"), {"user": "one"}),
|
||||
(("user-matcher", "https://host/user/two"), ("two", None)),
|
||||
(("user-matcher", "https://host/user/two/foo"), ("two", "foo")),
|
||||
]
|
||||
|
||||
should_not_match = [
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from typing import Dict, List, Sequence, Tuple, Type, Union
|
||||
from typing import Dict, List, Match, Optional, Sequence, Set, Tuple, Type, Union
|
||||
|
||||
from streamlink.plugin import Plugin
|
||||
import pytest
|
||||
|
||||
from streamlink.plugin.plugin import Matcher, Plugin
|
||||
|
||||
|
||||
generic_negative_matches = [
|
||||
|
@ -10,61 +12,158 @@ generic_negative_matches = [
|
|||
]
|
||||
|
||||
|
||||
# TODO: use proper type aliases
|
||||
TUrl = str
|
||||
TName = str
|
||||
TUrlNamed = Tuple[TName, TUrl]
|
||||
TUrlOrNamedUrl = Union[TUrl, TUrlNamed]
|
||||
TMatchGroup = Union[Dict[str, str], Sequence[Optional[str]]]
|
||||
|
||||
|
||||
_plugin_can_handle_url_classnames: Set[str] = set()
|
||||
|
||||
|
||||
class PluginCanHandleUrl:
|
||||
__plugin__: Type[Plugin]
|
||||
|
||||
# A list of URLs that should match any of the plugin's URL regexes.
|
||||
# ["https://foo", "https://bar"]
|
||||
should_match: List[str] = []
|
||||
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.
|
||||
|
||||
# A list of URL+capturegroup tuples, where capturegroup can be a dict (re.Match.groupdict()) or a tuple (re.Match.groups()).
|
||||
# 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 can be omitted in the test fixtures.
|
||||
# [("https://foo", {"foo": "foo"}), ("https://bar", ("bar", None))]
|
||||
should_match_groups: List[Union[Tuple[str, Dict], Tuple[str, Sequence]]] = []
|
||||
Example:
|
||||
should_match = [
|
||||
"https://foo",
|
||||
("bar", "https://bar"),
|
||||
]
|
||||
"""
|
||||
|
||||
# A list of URLs that should not match any of the plugin's URL regexes.
|
||||
# ["https://foo", "https://bar"]
|
||||
should_not_match: List[str] = []
|
||||
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 else
|
||||
# capture group tuples
|
||||
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 issubclass(self.__plugin__, Plugin), "Test has a __plugin__ that is a subclass of streamlink.plugin.Plugin"
|
||||
assert issubclass(getattr(self, "__plugin__"), Plugin), "Test has a __plugin__ that is a subclass of the Plugin class"
|
||||
assert len(self.should_match) + len(self.should_match_groups) > 0, "Test has at least one positive URL"
|
||||
|
||||
def test_matchers(self):
|
||||
should_match = self.should_match + [url for url, groups in self.should_match_groups]
|
||||
assert all(
|
||||
any(
|
||||
matcher.pattern.match(url)
|
||||
for url in should_match
|
||||
)
|
||||
for matcher in self.__plugin__.matchers
|
||||
), "All plugin matchers should match"
|
||||
def test_class_name(self, classnames: Set[str]):
|
||||
assert self.__class__.__name__ not in classnames
|
||||
|
||||
# parametrized dynamically via conftest.py
|
||||
def test_can_handle_url_positive(self, url):
|
||||
# ---- 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 matcher in self.__plugin__.matchers
|
||||
), "URL matches"
|
||||
for url in [(item if type(item) is str else item[1]) for item in self.urls_all()]
|
||||
), "Matcher matches at least one URL"
|
||||
|
||||
# parametrized dynamically via conftest.py
|
||||
def test_can_handle_url_negative(self, url):
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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.__plugin__.matchers
|
||||
), "URL does not match"
|
||||
|
||||
# parametrized dynamically via conftest.py
|
||||
def test_capture_groups(self, url, groups):
|
||||
for matcher in self.__plugin__.matchers:
|
||||
match = matcher.pattern.match(url)
|
||||
if match: # pragma: no branch
|
||||
res = (
|
||||
# ignore None values in capture group dicts
|
||||
{k: v for k, v in match.groupdict().items() if v is not None}
|
||||
if type(groups) is dict else
|
||||
# capture group tuples
|
||||
match.groups()
|
||||
)
|
||||
assert res == groups, "URL capture groups match"
|
||||
for matcher in self.matchers()
|
||||
), "URL does not match any matcher"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from typing import List
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.plugins import PluginCanHandleUrl, generic_negative_matches
|
||||
from streamlink.plugin.plugin import Matcher
|
||||
from tests.plugins import PluginCanHandleUrl
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: List[pytest.Item]): # pragma: no cover
|
||||
|
@ -17,16 +18,68 @@ def pytest_collection_modifyitems(items: List[pytest.Item]): # pragma: no cover
|
|||
]
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc): # pragma: no cover
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc):
|
||||
if metafunc.cls is not None and issubclass(metafunc.cls, PluginCanHandleUrl):
|
||||
if metafunc.function.__name__ == "test_can_handle_url_positive":
|
||||
metafunc.parametrize("url", metafunc.cls.should_match + [url for url, groups in metafunc.cls.should_match_groups])
|
||||
name: str = f"_parametrize_plugincanhandleurl_{metafunc.function.__name__}"
|
||||
parametrizer: Optional[Callable[[pytest.Metafunc], None]] = globals().get(name)
|
||||
if parametrizer:
|
||||
parametrizer(metafunc)
|
||||
|
||||
elif metafunc.function.__name__ == "test_can_handle_url_negative":
|
||||
metafunc.parametrize("url", metafunc.cls.should_not_match + generic_negative_matches)
|
||||
|
||||
elif metafunc.function.__name__ == "test_capture_groups":
|
||||
metafunc.parametrize("url,groups", metafunc.cls.should_match_groups, ids=[
|
||||
f"URL={url} GROUPS={groups}"
|
||||
for url, groups in metafunc.cls.should_match_groups
|
||||
])
|
||||
def _parametrize_plugincanhandleurl_test_all_matchers_match(metafunc: pytest.Metafunc):
|
||||
matchers: List[Tuple[int, Matcher]] = [(i, m) for i, m in enumerate(metafunc.cls.matchers())]
|
||||
metafunc.parametrize(
|
||||
"matcher",
|
||||
[m for i, m in matchers],
|
||||
ids=[m.name or f"#{i}" for i, m in matchers],
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_all_named_matchers_have_tests(metafunc: pytest.Metafunc):
|
||||
matchers: List[Matcher] = [m for m in metafunc.cls.matchers() if m.name is not None]
|
||||
metafunc.parametrize(
|
||||
"matcher",
|
||||
matchers,
|
||||
ids=[m.name for m in matchers],
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_url_matches_positive_unnamed(metafunc: pytest.Metafunc):
|
||||
metafunc.parametrize(
|
||||
"url",
|
||||
metafunc.cls.urls_unnamed(),
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_url_matches_positive_named(metafunc: pytest.Metafunc):
|
||||
urls = metafunc.cls.urls_named()
|
||||
metafunc.parametrize(
|
||||
"name,url",
|
||||
urls,
|
||||
ids=[f"NAME={name} URL={url}" for name, url in urls],
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_url_matches_groups_unnamed(metafunc: pytest.Metafunc):
|
||||
urlgroups = metafunc.cls.urlgroups_unnamed()
|
||||
metafunc.parametrize(
|
||||
"url,groups",
|
||||
urlgroups,
|
||||
ids=[f"URL={url} GROUPS={groups}" for url, groups in urlgroups],
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_url_matches_groups_named(metafunc: pytest.Metafunc):
|
||||
urlgroups = metafunc.cls.urlgroups_named()
|
||||
metafunc.parametrize(
|
||||
"name,url,groups",
|
||||
urlgroups,
|
||||
ids=[f"NAME={name} URL={url} GROUPS={groups}" for name, url, groups in urlgroups],
|
||||
)
|
||||
|
||||
|
||||
def _parametrize_plugincanhandleurl_test_url_matches_negative(metafunc: pytest.Metafunc):
|
||||
metafunc.parametrize(
|
||||
"url",
|
||||
metafunc.cls.urls_negative(),
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ from streamlink.plugins.goltelevision import GOLTelevision
|
|||
from tests.plugins import PluginCanHandleUrl
|
||||
|
||||
|
||||
class TestPluginCanHandleUrlEuronews(PluginCanHandleUrl):
|
||||
class TestPluginCanHandleUrlGOLTelevision(PluginCanHandleUrl):
|
||||
__plugin__ = GOLTelevision
|
||||
|
||||
should_match = [
|
||||
|
|
|
@ -2,7 +2,7 @@ from streamlink.plugins.lnk import LNK
|
|||
from tests.plugins import PluginCanHandleUrl
|
||||
|
||||
|
||||
class TestPluginCanHandleUrlLRT(PluginCanHandleUrl):
|
||||
class TestPluginCanHandleUrlLNK(PluginCanHandleUrl):
|
||||
__plugin__ = LNK
|
||||
|
||||
should_match = [
|
||||
|
|
|
@ -29,9 +29,9 @@ class TestPluginCanHandleUrlTVP(PluginCanHandleUrl):
|
|||
),
|
||||
|
||||
# tvp.info
|
||||
("https://tvp.info/", {}),
|
||||
("https://www.tvp.info/", {}),
|
||||
("https://www.tvp.info/65275202/13012023-0823", {}),
|
||||
(("tvp_info", "https://tvp.info/"), {}),
|
||||
(("tvp_info", "https://www.tvp.info/"), {}),
|
||||
(("tvp_info", "https://www.tvp.info/65275202/13012023-0823"), {}),
|
||||
]
|
||||
|
||||
should_not_match = [
|
||||
|
|
|
@ -2,7 +2,7 @@ from streamlink.plugins.tvtoya import TVToya
|
|||
from tests.plugins import PluginCanHandleUrl
|
||||
|
||||
|
||||
class TestPluginCanHandleUrlTVRPlus(PluginCanHandleUrl):
|
||||
class TestPluginCanHandleUrlTVToya(PluginCanHandleUrl):
|
||||
__plugin__ = TVToya
|
||||
|
||||
should_match = [
|
||||
|
|
Loading…
Reference in New Issue