Repository: streamlink/streamlink

Issue:
Tests are failing on test_cookie_clear_unbound
### Checklist

- [X] This is a bug report and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

I'm trying to build the package for 2.3.0 on Arch and the tests are failing on test_cookie_clear_unbound. This does look to be an issue with the test itself. I have attached the build log.

[streamlink-2.3.0-1-x86_64-check.log](https://github.com/streamlink/streamlink/files/7001827/streamlink-2.3.0-1-x86_64-check.log)


### Debug log

```text
This issue is on the build process.
```



Discussion:
The plugin cookie stuff hasn't been touched in three years since it was implemented in #1724:
- https://github.com/streamlink/streamlink/blame/2.3.0/tests/test_plugin.py#L133-L143
- https://github.com/streamlink/streamlink/blame/2.3.0/src/streamlink/plugin/plugin.py#L437-L520

The test checks whether a RuntimeError is being raised when the test plugin is "unbound" and no session object exists on it. Since the test fails, this means that the session object was set somewhere as a class attribute. There are lots of tests which don't have proper setups and pollute the state of certain objects, which means the execution depends on the order of tests being run, which is really bad.

Without having looked at the issue yet, I'm pretty sure that it's caused by some other test calling `Plugin.bind()` instead of calling `SomePluginImplementation.bind()`. Let me see...

Resolving patch:
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -480,7 +480,7 @@ def load_cookies(self):
         :return: list of the restored cookie names
         """
         if not self.session or not self.cache:
-            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")
+            raise RuntimeError("Cannot load cached cookies in unbound plugin")
 
         restored = []
 
@@ -504,7 +504,7 @@ def clear_cookies(self, cookie_filter=None):
         :return: list of the removed cookie names
         """
         if not self.session or not self.cache:
-            raise RuntimeError("Cannot loaded cached cookies in unbound plugin")
+            raise RuntimeError("Cannot clear cached cookies in unbound plugin")
 
         cookie_filter = cookie_filter or (lambda c: True)
         removed = []


Test patch:
diff --git a/tests/plugins/test_dash.py b/tests/plugins/test_dash.py
--- a/tests/plugins/test_dash.py
+++ b/tests/plugins/test_dash.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from streamlink.plugin.plugin import BIT_RATE_WEIGHT_RATIO, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY, Plugin
+from streamlink.plugin.plugin import BIT_RATE_WEIGHT_RATIO, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY
 from streamlink.plugins.dash import MPEGDASH
 from tests.plugins import PluginCanHandleUrl
 
@@ -26,7 +26,7 @@ class TestPluginCanHandleUrlMPEGDASH(PluginCanHandleUrl):
 ])
 def test_priority(url, priority):
     session = Mock()
-    Plugin.bind(session, "tests.plugins.test_dash")
+    MPEGDASH.bind(session, "tests.plugins.test_dash")
     assert next((matcher.priority for matcher in MPEGDASH.matchers if matcher.pattern.match(url)), NO_PRIORITY) == priority
 
 
@@ -41,7 +41,7 @@ def test_stream_weight(self):
     @patch("streamlink.stream.DASHStream.parse_manifest")
     def test_get_streams_prefix(self, parse_manifest):
         session = Mock()
-        Plugin.bind(session, "tests.plugins.test_dash")
+        MPEGDASH.bind(session, "tests.plugins.test_dash")
         p = MPEGDASH("dash://http://example.com/foo.mpd")
         p.streams()
         parse_manifest.assert_called_with(session, "http://example.com/foo.mpd")
@@ -49,7 +49,7 @@ def test_get_streams_prefix(self, parse_manifest):
     @patch("streamlink.stream.DASHStream.parse_manifest")
     def test_get_streams(self, parse_manifest):
         session = Mock()
-        Plugin.bind(session, "tests.plugins.test_dash")
+        MPEGDASH.bind(session, "tests.plugins.test_dash")
         p = MPEGDASH("http://example.com/foo.mpd")
         p.streams()
         parse_manifest.assert_called_with(session, "http://example.com/foo.mpd")
diff --git a/tests/plugins/test_filmon.py b/tests/plugins/test_filmon.py
--- a/tests/plugins/test_filmon.py
+++ b/tests/plugins/test_filmon.py
@@ -2,7 +2,6 @@
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.filmon import Filmon
 from tests.plugins import PluginCanHandleUrl
 
@@ -35,7 +34,7 @@ class TestPluginCanHandleUrlFilmon(PluginCanHandleUrl):
     ('http://www.filmon.tv/vod/view/10250-0-crime-boss', [None, None, '10250'])
 ])
 def test_match_url(url, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_filmon")
+    Filmon.bind(Mock(), "tests.plugins.test_filmon")
     plugin = Filmon(url)
     assert plugin.match is not None
     # expected must return [is_group, channel, vod_id]
diff --git a/tests/plugins/test_qq.py b/tests/plugins/test_qq.py
--- a/tests/plugins/test_qq.py
+++ b/tests/plugins/test_qq.py
@@ -2,7 +2,6 @@
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.qq import QQ
 from tests.plugins import PluginCanHandleUrl
 
@@ -31,7 +30,7 @@ class TestPluginCanHandleUrlQQ(PluginCanHandleUrl):
     ("http://m.live.qq.com/10039165", "room_id", "10039165")
 ])
 def test_match_url(url, group, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_qq")
+    QQ.bind(Mock(), "tests.plugins.test_qq")
     plugin = QQ(url)
     assert plugin.match is not None
     assert plugin.match.group(group) == expected
diff --git a/tests/plugins/test_youtube.py b/tests/plugins/test_youtube.py
--- a/tests/plugins/test_youtube.py
+++ b/tests/plugins/test_youtube.py
@@ -2,7 +2,6 @@
 
 import pytest
 
-from streamlink.plugin import Plugin
 from streamlink.plugins.youtube import YouTube
 from tests.plugins import PluginCanHandleUrl
 
@@ -54,7 +53,7 @@ class TestPluginCanHandleUrlYouTube(PluginCanHandleUrl):
     ("https://www.youtube.com/watch?v=aqz-KE-bpKQ", "video_id", "aqz-KE-bpKQ"),
 ])
 def test_match_url(url, group, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_youtube")
+    YouTube.bind(Mock(), "tests.plugins.test_youtube")
     plugin = YouTube(url)
     assert plugin.match is not None
     assert plugin.match.group(group) == expected
@@ -68,5 +67,5 @@ def test_match_url(url, group, expected):
     ("http://www.youtube.com/watch?v=0123456789A", "https://www.youtube.com/watch?v=0123456789A"),
 ])
 def test_translate_url(url, expected):
-    Plugin.bind(Mock(), "tests.plugins.test_youtube")
+    YouTube.bind(Mock(), "tests.plugins.test_youtube")
     assert YouTube(url).url == expected
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -132,15 +132,21 @@ def test_cookie_store_clear_filter(self):
 
     def test_cookie_load_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.load_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.load_cookies()
+        self.assertEqual(str(cm.exception), "Cannot load cached cookies in unbound plugin")
 
     def test_cookie_save_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.save_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.save_cookies()
+        self.assertEqual(str(cm.exception), "Cannot cache cookies in unbound plugin")
 
     def test_cookie_clear_unbound(self):
         plugin = Plugin("http://test.se")
-        self.assertRaises(RuntimeError, plugin.clear_cookies)
+        with self.assertRaises(RuntimeError) as cm:
+            plugin.clear_cookies()
+        self.assertEqual(str(cm.exception), "Cannot clear cached cookies in unbound plugin")
 
 
 class TestPluginMatcher(unittest.TestCase):