File: test_webbrowser.py

package info (click to toggle)
streamlink 8.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,568 kB
  • sloc: python: 51,299; sh: 184; makefile: 152
file content (158 lines) | stat: -rw-r--r-- 6,972 bytes parent folder | download | duplicates (4)
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
from __future__ import annotations

from contextlib import AbstractContextManager, nullcontext
from pathlib import Path
from signal import SIGTERM

import pytest
import trio

from streamlink.compat import BaseExceptionGroup, is_win32
from streamlink.webbrowser.exceptions import WebbrowserError
from streamlink.webbrowser.webbrowser import Webbrowser


class _FakeWebbrowser(Webbrowser):
    @classmethod
    def launch_args(cls) -> list[str]:
        return ["foo", "bar"]


class TestInit:
    @pytest.mark.parametrize(
        ("executable", "resolve_executable", "raises"),
        [
            pytest.param(
                None,
                None,
                pytest.raises(WebbrowserError, match=r"^Could not find web browser executable: Please set the path "),
                id="Failure with unset path",
            ),
            pytest.param(
                "custom",
                None,
                pytest.raises(WebbrowserError, match=r"^Invalid web browser executable: custom$"),
                id="Failure with custom path",
            ),
            pytest.param(
                None,
                "default",
                nullcontext(),
                id="Success with default path",
            ),
            pytest.param(
                "custom",
                "custom",
                nullcontext(),
                id="Success with custom path",
            ),
        ],
        indirect=["resolve_executable"],
    )
    def test_resolve_executable(self, resolve_executable, executable: str | None, raises: nullcontext):
        with raises:
            Webbrowser(executable=executable)

    def test_arguments(self):
        webbrowser = _FakeWebbrowser()
        assert webbrowser.executable == "default"
        assert webbrowser.arguments == ["foo", "bar"]
        assert webbrowser.arguments is not _FakeWebbrowser.launch_args()


class TestLaunch:
    @pytest.mark.trio()
    async def test_terminate_on_nursery_exit(self, caplog: pytest.LogCaptureFixture, webbrowser_launch):
        process: trio.Process
        async with webbrowser_launch() as (_nursery, process):
            assert process.poll() is None, "process is still running"

        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
        ]

    @pytest.mark.trio()
    async def test_terminate_on_nursery_cancellation(self, caplog: pytest.LogCaptureFixture, webbrowser_launch):
        nursery: trio.Nursery
        process: trio.Process
        async with webbrowser_launch() as (nursery, process):
            assert process.poll() is None, "process is still running"
            nursery.cancel_scope.cancel()

        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
        ]

    @pytest.mark.trio()
    async def test_terminate_on_nursery_timeout(self, caplog: pytest.LogCaptureFixture, mock_clock, webbrowser_launch):
        process: trio.Process
        async with webbrowser_launch(timeout=10) as (_nursery, process):
            assert process.poll() is None, "process is still running"
            mock_clock.jump(20)
            await trio.lowlevel.checkpoint()

        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "warning", "Web browser task group has timed out"),
            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
        ]

    @pytest.mark.trio()
    @pytest.mark.parametrize("exception", [KeyboardInterrupt, SystemExit])
    async def test_propagate_keyboardinterrupt_systemexit(self, caplog: pytest.LogCaptureFixture, webbrowser_launch, exception):
        process: trio.Process
        with pytest.raises(exception) as excinfo:  # noqa: PT012
            async with webbrowser_launch() as (_nursery, process):
                assert process.poll() is None, "process is still running"
                async with trio.open_nursery():
                    raise exception()

        assert isinstance(excinfo.value.__context__, BaseExceptionGroup)
        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
        ]

    @pytest.mark.trio()
    async def test_terminate_on_exception(self, caplog: pytest.LogCaptureFixture, webbrowser_launch):
        process: trio.Process
        with pytest.raises(BaseExceptionGroup) as excinfo:  # noqa: PT012
            async with webbrowser_launch() as (_nursery, process):
                assert process.poll() is None, "process is still running"
                async with trio.open_nursery():
                    raise ZeroDivisionError()

        assert excinfo.group_contains(ZeroDivisionError)
        assert process.poll() == (1 if is_win32 else -SIGTERM), "Process has been terminated"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "debug", "Waiting for web browser process to terminate"),
        ]

    # don't run on Windows, because of some weird flaky subprocess early-termination issues
    @pytest.mark.posix_only()
    @pytest.mark.trio()
    # don't check for non-zero exit codes - we don't care
    @pytest.mark.parametrize("exit_code", [0, 1])
    async def test_process_ended_early(self, caplog: pytest.LogCaptureFixture, webbrowser_launch, exit_code):
        process: trio.Process
        async with webbrowser_launch(timeout=10) as (_nursery, process):
            assert process.poll() is None, "process is still running"
            assert process.stdin
            await process.stdin.send_all(str(exit_code).encode() + b"\r\n")
            await trio.sleep(5)

        assert process.poll() == exit_code, "Process has ended with the right exit code"
        assert [(record.name, record.levelname, record.msg) for record in caplog.records] == [
            ("streamlink.webbrowser.webbrowser", "warning", "Web browser process ended early"),
        ]


def test_temp_dir():
    webbrowser = Webbrowser()
    temp_dir = webbrowser._create_temp_dir()
    assert isinstance(temp_dir, AbstractContextManager)
    with temp_dir as path:
        assert Path(path).exists()
    assert not Path(path).exists()