mirror of https://github.com/streamlink/streamlink
899 lines
35 KiB
Python
899 lines
35 KiB
Python
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from argparse import Namespace
|
|
from io import BytesIO
|
|
from pathlib import Path, PosixPath, WindowsPath
|
|
from textwrap import dedent
|
|
from unittest.mock import Mock, call, patch
|
|
|
|
import freezegun
|
|
import pytest
|
|
|
|
import streamlink_cli.main
|
|
import tests.resources
|
|
from streamlink.exceptions import PluginError, StreamError, StreamlinkDeprecationWarning
|
|
from streamlink.session import Streamlink
|
|
from streamlink.stream.stream import Stream
|
|
from streamlink_cli.compat import stdout
|
|
from streamlink_cli.main import (
|
|
Formatter,
|
|
NoPluginError,
|
|
create_output,
|
|
format_valid_streams,
|
|
handle_stream,
|
|
handle_url,
|
|
output_stream,
|
|
resolve_stream_name,
|
|
)
|
|
from streamlink_cli.output import FileOutput, PlayerOutput
|
|
from tests.plugin.testplugin import TestPlugin as _TestPlugin
|
|
|
|
|
|
# TODO: rewrite the entire mess
|
|
|
|
|
|
class FakePlugin(_TestPlugin):
|
|
__module__ = "fake"
|
|
_streams = {} # type: ignore
|
|
|
|
def streams(self, *args, **kwargs):
|
|
return self._streams
|
|
|
|
def _get_streams(self): # pragma: no cover
|
|
pass
|
|
|
|
|
|
class TestCLIMain(unittest.TestCase):
|
|
def test_resolve_stream_name(self):
|
|
a = Mock()
|
|
b = Mock()
|
|
c = Mock()
|
|
d = Mock()
|
|
e = Mock()
|
|
streams = {
|
|
"160p": a,
|
|
"360p": b,
|
|
"480p": c,
|
|
"720p": d,
|
|
"1080p": e,
|
|
"worst": b,
|
|
"best": d,
|
|
"worst-unfiltered": a,
|
|
"best-unfiltered": e,
|
|
}
|
|
|
|
assert resolve_stream_name(streams, "unknown") == "unknown"
|
|
assert resolve_stream_name(streams, "160p") == "160p"
|
|
assert resolve_stream_name(streams, "360p") == "360p"
|
|
assert resolve_stream_name(streams, "480p") == "480p"
|
|
assert resolve_stream_name(streams, "720p") == "720p"
|
|
assert resolve_stream_name(streams, "1080p") == "1080p"
|
|
assert resolve_stream_name(streams, "worst") == "360p"
|
|
assert resolve_stream_name(streams, "best") == "720p"
|
|
assert resolve_stream_name(streams, "worst-unfiltered") == "160p"
|
|
assert resolve_stream_name(streams, "best-unfiltered") == "1080p"
|
|
|
|
def test_format_valid_streams(self):
|
|
a = Mock()
|
|
b = Mock()
|
|
c = Mock()
|
|
|
|
streams = {
|
|
"audio": a,
|
|
"720p": b,
|
|
"1080p": c,
|
|
"worst": b,
|
|
"best": c,
|
|
}
|
|
assert format_valid_streams(_TestPlugin, streams) == ", ".join([
|
|
"audio",
|
|
"720p (worst)",
|
|
"1080p (best)",
|
|
])
|
|
|
|
streams = {
|
|
"audio": a,
|
|
"720p": b,
|
|
"1080p": c,
|
|
"worst-unfiltered": b,
|
|
"best-unfiltered": c,
|
|
}
|
|
assert format_valid_streams(_TestPlugin, streams) == ", ".join([
|
|
"audio",
|
|
"720p (worst-unfiltered)",
|
|
"1080p (best-unfiltered)",
|
|
])
|
|
|
|
|
|
class TestCLIMainHandleUrl:
|
|
@pytest.mark.parametrize(("side_effect", "expected"), [
|
|
(NoPluginError("foo"), "No plugin can handle URL: fakeurl"),
|
|
(PluginError("bar"), "bar"),
|
|
])
|
|
def test_error(self, side_effect, expected):
|
|
with patch("streamlink_cli.main.args", Mock(url="fakeurl")), \
|
|
patch("streamlink_cli.main.streamlink", resolve_url=Mock(side_effect=side_effect)), \
|
|
patch("streamlink_cli.main.console", exit=Mock(side_effect=SystemExit)) as mock_console:
|
|
with pytest.raises(SystemExit):
|
|
handle_url()
|
|
assert mock_console.exit.mock_calls == [call(expected)]
|
|
|
|
|
|
class TestCLIMainJsonAndStreamUrl(unittest.TestCase):
|
|
@patch("streamlink_cli.main.args", json=True, stream_url=True, subprocess_cmdline=False)
|
|
@patch("streamlink_cli.main.console")
|
|
def test_handle_stream_with_json_and_stream_url(self, console, args):
|
|
stream = Mock()
|
|
streams = dict(best=stream)
|
|
|
|
plugin = FakePlugin(Mock(), "")
|
|
plugin._streams = streams
|
|
|
|
handle_stream(plugin, streams, "best")
|
|
assert console.msg.mock_calls == []
|
|
assert console.msg_json.mock_calls == [call(
|
|
stream,
|
|
metadata=dict(
|
|
id="test-id-1234-5678",
|
|
author="Tѥst Āuƭhǿr",
|
|
category=None,
|
|
title="Test Title",
|
|
),
|
|
)]
|
|
assert console.error.mock_calls == []
|
|
console.msg_json.mock_calls.clear()
|
|
|
|
args.json = False
|
|
handle_stream(plugin, streams, "best")
|
|
assert console.msg.mock_calls == [call(stream.to_url())]
|
|
assert console.msg_json.mock_calls == []
|
|
assert console.error.mock_calls == []
|
|
console.msg.mock_calls.clear()
|
|
|
|
stream.to_url.side_effect = TypeError()
|
|
handle_stream(plugin, streams, "best")
|
|
assert console.msg.mock_calls == []
|
|
assert console.msg_json.mock_calls == []
|
|
assert console.exit.mock_calls == [call("The stream specified cannot be translated to a URL")]
|
|
|
|
@patch("streamlink_cli.main.args", json=True, stream_url=True, stream=[], default_stream=[], retry_max=0, retry_streams=0)
|
|
@patch("streamlink_cli.main.console")
|
|
def test_handle_url_with_json_and_stream_url(self, console, args):
|
|
stream = Mock()
|
|
streams = dict(worst=Mock(), best=stream)
|
|
|
|
class _FakePlugin(FakePlugin):
|
|
__module__ = FakePlugin.__module__
|
|
_streams = streams
|
|
|
|
with patch("streamlink_cli.main.streamlink", resolve_url=Mock(return_value=("fake", _FakePlugin, ""))):
|
|
handle_url()
|
|
assert console.msg.mock_calls == []
|
|
assert console.msg_json.mock_calls == [call(
|
|
plugin="fake",
|
|
metadata=dict(
|
|
id="test-id-1234-5678",
|
|
author="Tѥst Āuƭhǿr",
|
|
category=None,
|
|
title="Test Title",
|
|
),
|
|
streams=streams,
|
|
)]
|
|
assert console.error.mock_calls == []
|
|
console.msg_json.mock_calls.clear()
|
|
|
|
args.json = False
|
|
handle_url()
|
|
assert console.msg.mock_calls == [call(stream.to_manifest_url())]
|
|
assert console.msg_json.mock_calls == []
|
|
assert console.error.mock_calls == []
|
|
console.msg.mock_calls.clear()
|
|
|
|
stream.to_manifest_url.side_effect = TypeError()
|
|
handle_url()
|
|
assert console.msg.mock_calls == []
|
|
assert console.msg_json.mock_calls == []
|
|
assert console.exit.mock_calls == [call("The stream specified cannot be translated to a URL")]
|
|
console.exit.mock_calls.clear()
|
|
|
|
|
|
# TODO: don't use Mock() for mocking args, use a custom argparse.Namespace instead
|
|
class TestCLIMainCreateOutput(unittest.TestCase):
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.console", Mock())
|
|
@patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
|
|
def test_create_output_no_file_output_options(self, args: Mock):
|
|
formatter = Formatter({
|
|
"author": lambda: "foo",
|
|
})
|
|
args.output = None
|
|
args.stdout = None
|
|
args.record = None
|
|
args.record_and_pipe = None
|
|
args.player_fifo = False
|
|
args.player_http = False
|
|
args.title = None
|
|
args.url = "URL"
|
|
args.player = Path("mpv")
|
|
args.player_args = ""
|
|
args.player_env = None
|
|
|
|
output = create_output(formatter)
|
|
assert type(output) is PlayerOutput
|
|
assert output.playerargs.title == "URL"
|
|
assert output.env == {}
|
|
|
|
args.title = "{author} - {title}"
|
|
output = create_output(formatter)
|
|
assert type(output) is PlayerOutput
|
|
assert output.playerargs.title == "foo - bar"
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.check_file_output")
|
|
def test_create_output_file_output(self, mock_check_file_output: Mock, args: Mock):
|
|
formatter = Formatter({})
|
|
mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
|
|
args.output = "foo"
|
|
args.stdout = None
|
|
args.record = None
|
|
args.record_and_pipe = None
|
|
args.force = False
|
|
args.fs_safe_rules = None
|
|
|
|
output = create_output(formatter)
|
|
assert mock_check_file_output.call_args_list == [call(Path("foo"), False)]
|
|
assert type(output) is FileOutput
|
|
assert output.filename == Path("foo")
|
|
assert output.fd is None
|
|
assert output.record is None
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
def test_create_output_stdout(self, args: Mock):
|
|
formatter = Formatter({})
|
|
args.output = None
|
|
args.stdout = True
|
|
args.record = None
|
|
args.record_and_pipe = None
|
|
|
|
output = create_output(formatter)
|
|
assert type(output) is FileOutput
|
|
assert output.filename is None
|
|
assert output.fd is stdout
|
|
assert output.record is None
|
|
|
|
args.output = "-"
|
|
args.stdout = False
|
|
output = create_output(formatter)
|
|
assert type(output) is FileOutput
|
|
assert output.filename is None
|
|
assert output.fd is stdout
|
|
assert output.record is None
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.check_file_output")
|
|
def test_create_output_record_and_pipe(self, mock_check_file_output: Mock, args: Mock):
|
|
formatter = Formatter({})
|
|
mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
|
|
args.output = None
|
|
args.stdout = None
|
|
args.record_and_pipe = "foo"
|
|
args.force = False
|
|
args.fs_safe_rules = None
|
|
|
|
output = create_output(formatter)
|
|
assert mock_check_file_output.call_args_list == [call(Path("foo"), False)]
|
|
assert type(output) is FileOutput
|
|
assert output.filename is None
|
|
assert output.fd is stdout
|
|
assert type(output.record) is FileOutput
|
|
assert output.record.filename == Path("foo")
|
|
assert output.record.fd is None
|
|
assert output.record.record is None
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.check_file_output")
|
|
@patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
|
|
def test_create_output_record(self, mock_check_file_output: Mock, args: Mock):
|
|
formatter = Formatter({
|
|
"author": lambda: "foo",
|
|
})
|
|
mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
|
|
args.output = None
|
|
args.stdout = None
|
|
args.record = "foo"
|
|
args.record_and_pipe = None
|
|
args.force = False
|
|
args.fs_safe_rules = None
|
|
args.title = None
|
|
args.url = "URL"
|
|
args.player = Path("mpv")
|
|
args.player_args = ""
|
|
args.player_env = [("VAR1", "abc"), ("VAR2", "def")]
|
|
args.player_fifo = None
|
|
args.player_http = None
|
|
|
|
output = create_output(formatter)
|
|
assert type(output) is PlayerOutput
|
|
assert output.playerargs.title == "URL"
|
|
assert output.env == {"VAR1": "abc", "VAR2": "def"}
|
|
assert type(output.record) is FileOutput
|
|
assert output.record.filename == Path("foo")
|
|
assert output.record.fd is None
|
|
assert output.record.record is None
|
|
|
|
args.title = "{author} - {title}"
|
|
output = create_output(formatter)
|
|
assert type(output) is PlayerOutput
|
|
assert output.playerargs.title == "foo - bar"
|
|
assert type(output.record) is FileOutput
|
|
assert output.record.filename == Path("foo")
|
|
assert output.record.fd is None
|
|
assert output.record.record is None
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
|
|
def test_create_output_record_stdout(self, args: Mock):
|
|
formatter = Formatter({
|
|
"author": lambda: "foo",
|
|
})
|
|
args.output = None
|
|
args.stdout = None
|
|
args.record = "-"
|
|
args.record_and_pipe = None
|
|
args.force = False
|
|
args.fs_safe_rules = None
|
|
args.title = "{author} - {title}"
|
|
args.url = "URL"
|
|
args.player = Path("mpv")
|
|
args.player_args = ""
|
|
args.player_env = [("VAR1", "abc"), ("VAR2", "def")]
|
|
args.player_fifo = None
|
|
args.player_http = None
|
|
|
|
output = create_output(formatter)
|
|
assert type(output) is PlayerOutput
|
|
assert output.playerargs.title == "foo - bar"
|
|
assert output.env == {"VAR1": "abc", "VAR2": "def"}
|
|
assert type(output.record) is FileOutput
|
|
assert output.record.filename is None
|
|
assert output.record.fd is stdout
|
|
assert output.record.record is None
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.console")
|
|
def test_create_output_record_and_other_file_output(self, console: Mock, args: Mock):
|
|
formatter = Formatter({})
|
|
args.output = None
|
|
args.stdout = True
|
|
args.record_and_pipe = True
|
|
create_output(formatter)
|
|
console.exit.assert_called_with("Cannot use record options with other file output options.")
|
|
|
|
@patch("streamlink_cli.main.args")
|
|
@patch("streamlink_cli.main.console")
|
|
def test_create_output_no_default_player(self, console: Mock, args: Mock):
|
|
formatter = Formatter({})
|
|
args.output = None
|
|
args.stdout = False
|
|
args.record_and_pipe = False
|
|
args.player = None
|
|
console.exit.side_effect = SystemExit
|
|
with pytest.raises(SystemExit):
|
|
create_output(formatter)
|
|
assert re.search(r"^The default player \(\w+\) does not seem to be installed\.", console.exit.call_args_list[0][0][0])
|
|
|
|
|
|
class TestCLIMainHandleStream(unittest.TestCase):
|
|
@patch("streamlink_cli.main.output_stream")
|
|
@patch("streamlink_cli.main.args")
|
|
def test_handle_stream_output_stream(self, args: Mock, mock_output_stream: Mock):
|
|
args.json = False
|
|
args.subprocess_cmdline = False
|
|
args.stream_url = False
|
|
args.output = False
|
|
args.stdout = False
|
|
args.player_passthrough = []
|
|
args.player_external_http = False
|
|
args.player_continuous_http = False
|
|
mock_output_stream.return_value = True
|
|
|
|
session = Mock()
|
|
plugin = FakePlugin(session, "")
|
|
stream = Stream(session)
|
|
streams = {"best": stream}
|
|
|
|
handle_stream(plugin, streams, "best")
|
|
assert mock_output_stream.call_count == 1
|
|
paramStream, paramFormatter = mock_output_stream.call_args[0]
|
|
assert paramStream is stream
|
|
assert isinstance(paramFormatter, Formatter)
|
|
|
|
|
|
class TestCLIMainOutputStream:
|
|
def test_stream_failure_no_output_open(self, caplog: pytest.LogCaptureFixture):
|
|
output = Mock()
|
|
stream = Mock(
|
|
__str__=lambda _: "fake-stream",
|
|
open=Mock(side_effect=StreamError("failure")),
|
|
)
|
|
formatter = Mock()
|
|
|
|
caplog.set_level(1, "streamlink.cli")
|
|
|
|
with patch("streamlink_cli.main.args", Namespace(retry_open=2)), \
|
|
patch("streamlink_cli.main.console") as mock_console, \
|
|
patch("streamlink_cli.main.output"), \
|
|
patch("streamlink_cli.main.create_output", return_value=output):
|
|
output_stream(stream, formatter)
|
|
|
|
assert [(record.levelname, record.module, record.message) for record in caplog.records] == [
|
|
("error", "main", "Try 1/2: Could not open stream fake-stream (Could not open stream: failure)"),
|
|
("error", "main", "Try 2/2: Could not open stream fake-stream (Could not open stream: failure)"),
|
|
]
|
|
assert mock_console.exit.call_args_list == [
|
|
call("Could not open stream fake-stream, tried 2 times, exiting"),
|
|
]
|
|
assert not output.open.called, "Does not open the output on stream error"
|
|
|
|
@pytest.mark.parametrize(
|
|
("args", "isatty", "deprecation", "expected"),
|
|
[
|
|
({"progress": "yes", "force_progress": False}, True, False, True),
|
|
({"progress": "no", "force_progress": False}, True, False, False),
|
|
({"progress": "yes", "force_progress": False}, False, False, False),
|
|
({"progress": "no", "force_progress": False}, False, False, False),
|
|
({"progress": "force", "force_progress": False}, False, False, True),
|
|
({"progress": "yes", "force_progress": True}, False, True, True),
|
|
({"progress": "no", "force_progress": True}, False, True, True),
|
|
],
|
|
)
|
|
def test_show_progress(
|
|
self,
|
|
caplog: pytest.LogCaptureFixture,
|
|
recwarn: pytest.WarningsRecorder,
|
|
args: dict,
|
|
isatty: bool,
|
|
deprecation: bool,
|
|
expected: bool,
|
|
):
|
|
streamio = BytesIO(b"0" * 8192 * 2)
|
|
stream = Mock(open=Mock(return_value=streamio))
|
|
output = Mock()
|
|
formatter = Mock()
|
|
|
|
caplog.set_level(1, "streamlink.cli")
|
|
|
|
with patch("streamlink_cli.main.sys.stderr.isatty", return_value=isatty), \
|
|
patch("streamlink_cli.main.args", Namespace(retry_open=1, **args)), \
|
|
patch("streamlink_cli.main.console") as mock_console, \
|
|
patch("streamlink_cli.main.output"), \
|
|
patch("streamlink_cli.main.create_output", return_value=output), \
|
|
patch("streamlink_cli.main.StreamRunner") as mock_streamrunner:
|
|
assert output_stream(stream, formatter)
|
|
|
|
assert not mock_console.exit.called
|
|
assert [(record.levelname, record.module, record.message) for record in caplog.records] == [
|
|
("debug", "main", "Pre-buffering 8192 bytes"),
|
|
("debug", "main", "Writing stream to output"),
|
|
]
|
|
assert [(record.category, str(record.message)) for record in recwarn.list] == ([(
|
|
StreamlinkDeprecationWarning,
|
|
"The --force-progress option has been deprecated in favor of --progress=force",
|
|
)] if deprecation else [])
|
|
assert mock_streamrunner.call_args_list == [call(streamio, output, show_progress=expected)]
|
|
|
|
|
|
# TODO: rewrite using pytest (caplog+capsys fixtures) and move to separate test module
|
|
class _TestCLIMainLogging(unittest.TestCase):
|
|
# stop test execution at the setup_signals() call, as we're not interested in what comes afterwards
|
|
class StopTest(Exception):
|
|
pass
|
|
|
|
@classmethod
|
|
def subject(cls, argv, **kwargs):
|
|
session = Streamlink(plugins_builtin=False)
|
|
session.plugins.load_path(Path(tests.__path__[0]) / "plugin")
|
|
|
|
with patch("streamlink_cli.main.os.geteuid", create=True, new=Mock(return_value=kwargs.get("euid", 1000))), \
|
|
patch("streamlink_cli.main.streamlink", session), \
|
|
patch("streamlink_cli.main.setup_signals", side_effect=cls.StopTest), \
|
|
patch("streamlink_cli.main.CONFIG_FILES", []), \
|
|
patch("streamlink_cli.main.setup_streamlink"), \
|
|
patch("streamlink_cli.main.setup_plugins"), \
|
|
patch("streamlink_cli.argparser.find_default_player"), \
|
|
patch("sys.argv") as mock_argv:
|
|
mock_argv.__getitem__.side_effect = lambda x: argv[x]
|
|
try:
|
|
streamlink_cli.main.main()
|
|
except cls.StopTest:
|
|
pass
|
|
|
|
def tearDown(self):
|
|
streamlink_cli.main.logger.root.handlers.clear()
|
|
|
|
_write_call_log_cli_info = [call("[cli][info] foo\n")]
|
|
_write_call_console_msg = [call("bar\n")]
|
|
_write_call_console_msg_error = [call("error: bar\n")]
|
|
_write_call_console_msg_json = [call("{\n \"error\": \"bar\"\n}\n")]
|
|
|
|
_write_calls = _write_call_log_cli_info + _write_call_console_msg
|
|
|
|
def write_file_and_assert(self, mock_mkdir: Mock, mock_write: Mock, mock_stdout: Mock):
|
|
streamlink_cli.main.log.info("foo")
|
|
streamlink_cli.main.console.msg("bar")
|
|
assert mock_mkdir.mock_calls == [call(parents=True, exist_ok=True)]
|
|
assert mock_write.mock_calls == self._write_calls
|
|
assert not mock_stdout.write.called
|
|
|
|
|
|
class TestCLIMainLoggingStreams(_TestCLIMainLogging):
|
|
_write_call_log_testcli_err = [call("[test_cli_main][error] baz\n")]
|
|
|
|
def subject(self, argv, stream=None):
|
|
super().subject(argv)
|
|
childlogger = logging.getLogger("streamlink.test_cli_main")
|
|
|
|
streamlink_cli.main.log.info("foo")
|
|
childlogger.error("baz")
|
|
with pytest.raises(SystemExit):
|
|
streamlink_cli.main.console.exit("bar")
|
|
|
|
assert streamlink_cli.main.log.parent.handlers[0].stream is stream
|
|
assert childlogger.parent.handlers[0].stream is stream
|
|
assert streamlink_cli.main.console.output is stream
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_stdout(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--stdout"], mock_stderr)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_output_eq_file(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--output=foo"], mock_stdout)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_output_eq_dash(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--output=-"], mock_stderr)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_record_eq_file(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--record=foo"], mock_stdout)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_record_eq_dash(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--record=-"], mock_stderr)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_stream_record_and_pipe(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--record-and-pipe=foo"], mock_stderr)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_no_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink"], mock_stdout)
|
|
assert mock_stdout.write.mock_calls == (
|
|
self._write_call_log_cli_info
|
|
+ self._write_call_log_testcli_err
|
|
+ self._write_call_console_msg_error
|
|
)
|
|
assert mock_stderr.write.mock_calls == []
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_no_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--json"], mock_stdout)
|
|
assert mock_stdout.write.mock_calls == self._write_call_console_msg_json
|
|
assert mock_stderr.write.mock_calls == []
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--stdout"], mock_stderr)
|
|
assert mock_stdout.write.mock_calls == []
|
|
assert mock_stderr.write.mock_calls == (
|
|
self._write_call_log_cli_info
|
|
+ self._write_call_log_testcli_err
|
|
+ self._write_call_console_msg_error
|
|
)
|
|
|
|
@patch("sys.stderr")
|
|
@patch("sys.stdout")
|
|
def test_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock):
|
|
self.subject(["streamlink", "--stdout", "--json"], mock_stderr)
|
|
assert mock_stdout.write.mock_calls == []
|
|
assert mock_stderr.write.mock_calls == self._write_call_console_msg_json
|
|
|
|
|
|
class TestCLIMainLoggingInfos(_TestCLIMainLogging):
|
|
@pytest.mark.posix_only()
|
|
@patch("streamlink_cli.main.log")
|
|
def test_log_root_warning(self, mock_log):
|
|
self.subject(["streamlink"], euid=0)
|
|
assert mock_log.info.mock_calls == [call("streamlink is running as root! Be careful!")]
|
|
|
|
@patch("streamlink_cli.main.log")
|
|
@patch("streamlink_cli.main.streamlink_version", "streamlink")
|
|
@patch("streamlink_cli.main.importlib.metadata")
|
|
@patch("streamlink_cli.main.log_current_arguments", Mock(side_effect=_TestCLIMainLogging.StopTest))
|
|
@patch("platform.python_version", Mock(return_value="python"))
|
|
@patch("ssl.OPENSSL_VERSION", "OPENSSL_VERSION")
|
|
def test_log_current_versions(self, mock_importlib_metadata: Mock, mock_log: Mock):
|
|
class FakePackageNotFoundError(Exception):
|
|
pass
|
|
|
|
def version(dist):
|
|
if dist == "foo":
|
|
return "1.2.3"
|
|
if dist == "bar-baz":
|
|
return "2.0.0"
|
|
raise FakePackageNotFoundError()
|
|
|
|
mock_importlib_metadata.PackageNotFoundError = FakePackageNotFoundError
|
|
mock_importlib_metadata.requires.return_value = ["foo>1", "bar-baz==2", "qux~=3"]
|
|
mock_importlib_metadata.version.side_effect = version
|
|
|
|
self.subject(["streamlink", "--loglevel", "info"])
|
|
assert mock_log.debug.mock_calls == [], "Doesn't log anything if not debug logging"
|
|
|
|
with patch("sys.platform", "linux"), \
|
|
patch("platform.platform", Mock(return_value="linux")):
|
|
self.subject(["streamlink", "--loglevel", "debug"])
|
|
assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
|
|
assert mock_log.debug.mock_calls == [
|
|
call("OS: linux"),
|
|
call("Python: python"),
|
|
call("OpenSSL: OPENSSL_VERSION"),
|
|
call("Streamlink: streamlink"),
|
|
call("Dependencies:"),
|
|
call(" foo: 1.2.3"),
|
|
call(" bar-baz: 2.0.0"),
|
|
]
|
|
mock_importlib_metadata.requires.reset_mock()
|
|
mock_log.debug.reset_mock()
|
|
|
|
with patch("sys.platform", "darwin"), \
|
|
patch("platform.mac_ver", Mock(return_value=["0.0.0"])):
|
|
self.subject(["streamlink", "--loglevel", "debug"])
|
|
assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
|
|
assert mock_log.debug.mock_calls == [
|
|
call("OS: macOS 0.0.0"),
|
|
call("Python: python"),
|
|
call("OpenSSL: OPENSSL_VERSION"),
|
|
call("Streamlink: streamlink"),
|
|
call("Dependencies:"),
|
|
call(" foo: 1.2.3"),
|
|
call(" bar-baz: 2.0.0"),
|
|
]
|
|
mock_importlib_metadata.requires.reset_mock()
|
|
mock_log.debug.reset_mock()
|
|
|
|
with patch("sys.platform", "win32"), \
|
|
patch("platform.system", Mock(return_value="Windows")), \
|
|
patch("platform.release", Mock(return_value="0.0.0")):
|
|
self.subject(["streamlink", "--loglevel", "debug"])
|
|
assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
|
|
assert mock_log.debug.mock_calls == [
|
|
call("OS: Windows 0.0.0"),
|
|
call("Python: python"),
|
|
call("OpenSSL: OPENSSL_VERSION"),
|
|
call("Streamlink: streamlink"),
|
|
call("Dependencies:"),
|
|
call(" foo: 1.2.3"),
|
|
call(" bar-baz: 2.0.0"),
|
|
]
|
|
mock_importlib_metadata.requires.reset_mock()
|
|
mock_log.debug.reset_mock()
|
|
|
|
@patch("streamlink_cli.main.log")
|
|
def test_log_current_arguments(self, mock_log):
|
|
self.subject([
|
|
"streamlink",
|
|
"--loglevel", "info",
|
|
])
|
|
assert mock_log.debug.mock_calls == [], "Doesn't log anything if not debug logging"
|
|
|
|
self.subject([
|
|
"streamlink",
|
|
"--loglevel", "debug",
|
|
"-p", "custom",
|
|
"--testplugin-bool",
|
|
"--testplugin-password=secret",
|
|
"test.se/channel",
|
|
"best,worst",
|
|
])
|
|
assert mock_log.debug.mock_calls[-7:] == [
|
|
call("Arguments:"),
|
|
call(" url=test.se/channel"),
|
|
call(" stream=['best', 'worst']"),
|
|
call(" --loglevel=debug"),
|
|
call(" --player=custom"),
|
|
call(" --testplugin-bool=True"),
|
|
call(" --testplugin-password=********"),
|
|
]
|
|
|
|
|
|
class TestCLIMainLoggingLogfile(_TestCLIMainLogging):
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
def test_logfile_no_logfile(self, mock_open, mock_stdout):
|
|
self.subject(["streamlink"])
|
|
streamlink_cli.main.log.info("foo")
|
|
streamlink_cli.main.console.msg("bar")
|
|
assert streamlink_cli.main.console.output == sys.stdout
|
|
assert not mock_open.called
|
|
assert mock_stdout.write.mock_calls == self._write_calls
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
def test_logfile_loglevel_none(self, mock_open, mock_stdout):
|
|
self.subject(["streamlink", "--loglevel", "none", "--logfile", "foo"])
|
|
streamlink_cli.main.log.info("foo")
|
|
streamlink_cli.main.console.msg("bar")
|
|
assert streamlink_cli.main.console.output == sys.stdout
|
|
assert not mock_open.called
|
|
assert mock_stdout.write.mock_calls == [call("bar\n")]
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
def test_logfile_path_relative(self, mock_open, mock_stdout):
|
|
path = Path("foo").resolve()
|
|
self.subject(["streamlink", "--logfile", "foo"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=path.mkdir,
|
|
mock_write=mock_open(str(path), "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
|
|
@pytest.mark.posix_only()
|
|
class TestCLIMainLoggingLogfilePosix(_TestCLIMainLogging):
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
def test_logfile_path_absolute(self, mock_open, mock_stdout):
|
|
self.subject(["streamlink", "--logfile", "/foo/bar"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=PosixPath("/foo").mkdir,
|
|
mock_write=mock_open("/foo/bar", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
def test_logfile_path_expanduser(self, mock_open, mock_stdout):
|
|
with patch.dict(os.environ, {"HOME": "/foo"}):
|
|
self.subject(["streamlink", "--logfile", "~/bar"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=PosixPath("/foo").mkdir,
|
|
mock_write=mock_open("/foo/bar", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
@freezegun.freeze_time("2000-01-02T03:04:05Z")
|
|
def test_logfile_path_auto(self, mock_open, mock_stdout):
|
|
with patch("streamlink_cli.constants.LOG_DIR", PosixPath("/foo")):
|
|
self.subject(["streamlink", "--logfile", "-"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=PosixPath("/foo").mkdir,
|
|
mock_write=mock_open("/foo/2000-01-02_03-04-05.log", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
|
|
@pytest.mark.windows_only()
|
|
class TestCLIMainLoggingLogfileWindows(_TestCLIMainLogging):
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
def test_logfile_path_absolute(self, mock_open, mock_stdout):
|
|
self.subject(["streamlink", "--logfile", "C:\\foo\\bar"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=WindowsPath("C:\\foo").mkdir,
|
|
mock_write=mock_open("C:\\foo\\bar", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
def test_logfile_path_expanduser(self, mock_open, mock_stdout):
|
|
with patch.dict(os.environ, {"USERPROFILE": "C:\\foo"}):
|
|
self.subject(["streamlink", "--logfile", "~\\bar"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=WindowsPath("C:\\foo").mkdir,
|
|
mock_write=mock_open("C:\\foo\\bar", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
@patch("sys.stdout")
|
|
@patch("builtins.open")
|
|
@patch("pathlib.Path.mkdir", Mock())
|
|
@freezegun.freeze_time("2000-01-02T03:04:05Z")
|
|
def test_logfile_path_auto(self, mock_open, mock_stdout):
|
|
with patch("streamlink_cli.constants.LOG_DIR", WindowsPath("C:\\foo")):
|
|
self.subject(["streamlink", "--logfile", "-"])
|
|
self.write_file_and_assert(
|
|
mock_mkdir=WindowsPath("C:\\foo").mkdir,
|
|
mock_write=mock_open("C:\\foo\\2000-01-02_03-04-05.log", "a").write,
|
|
mock_stdout=mock_stdout,
|
|
)
|
|
|
|
|
|
class TestCLIMainPrint(unittest.TestCase):
|
|
def subject(self):
|
|
with patch.object(Streamlink, "resolve_url") as mock_resolve_url, \
|
|
patch.object(Streamlink, "resolve_url_no_redirect") as mock_resolve_url_no_redirect:
|
|
session = Streamlink(plugins_builtin=False)
|
|
session.plugins.load_path(Path(tests.__path__[0]) / "plugin")
|
|
with patch("streamlink_cli.main.os.geteuid", create=True, new=Mock(return_value=1000)), \
|
|
patch("streamlink_cli.main.streamlink", session), \
|
|
patch("streamlink_cli.main.CONFIG_FILES", []), \
|
|
patch("streamlink_cli.main.setup_streamlink"), \
|
|
patch("streamlink_cli.main.setup_plugins"), \
|
|
patch("streamlink_cli.main.setup_signals"):
|
|
with pytest.raises(SystemExit) as cm:
|
|
streamlink_cli.main.main()
|
|
assert cm.value.code == 0
|
|
mock_resolve_url.assert_not_called()
|
|
mock_resolve_url_no_redirect.assert_not_called()
|
|
|
|
def tearDown(self):
|
|
streamlink_cli.main.logger.root.handlers.clear()
|
|
|
|
@staticmethod
|
|
def get_stdout(mock_stdout):
|
|
return "".join([call_arg[0][0] for call_arg in mock_stdout.write.call_args_list])
|
|
|
|
@patch("sys.stdout")
|
|
@patch("sys.argv", ["streamlink"])
|
|
def test_print_usage(self, mock_stdout):
|
|
self.subject()
|
|
assert self.get_stdout(mock_stdout) == dedent("""
|
|
usage: streamlink [OPTIONS] <URL> [STREAM]
|
|
|
|
Use -h/--help to see the available options or read the manual at https://streamlink.github.io
|
|
""").lstrip()
|
|
|
|
@patch("sys.stdout")
|
|
@patch("sys.argv", ["streamlink", "--help"])
|
|
def test_print_help(self, mock_stdout):
|
|
self.subject()
|
|
output = self.get_stdout(mock_stdout)
|
|
assert "usage: streamlink [OPTIONS] <URL> [STREAM]" in output
|
|
assert dedent("""
|
|
Streamlink is a command-line utility that extracts streams from various
|
|
services and pipes them into a video player of choice.
|
|
""") in output
|
|
assert dedent("""
|
|
For more in-depth documentation see:
|
|
https://streamlink.github.io
|
|
|
|
Please report broken plugins or bugs to the issue tracker on Github:
|
|
https://github.com/streamlink/streamlink/issues
|
|
""") in output
|
|
|
|
@patch("sys.stdout")
|
|
@patch("sys.argv", ["streamlink", "--plugins"])
|
|
def test_print_plugins(self, mock_stdout):
|
|
self.subject()
|
|
assert self.get_stdout(mock_stdout) == "Loaded plugins: testplugin\n"
|
|
|
|
@patch("sys.stdout")
|
|
@patch("sys.argv", ["streamlink", "--plugins", "--json"])
|
|
def test_print_plugins_json(self, mock_stdout):
|
|
self.subject()
|
|
assert self.get_stdout(mock_stdout) == '[\n "testplugin"\n]\n'
|