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:
bastimeyer 2023-01-21 14:18:14 +01:00 committed by Sebastian Meyer
parent e629975e71
commit 66244e32d7
7 changed files with 225 additions and 67 deletions

View File

@ -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 = [

View File

@ -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"

View File

@ -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(),
)

View File

@ -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 = [

View File

@ -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 = [

View File

@ -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 = [

View File

@ -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 = [