mirror of https://github.com/streamlink/streamlink
Windows command line parsing fix (#300)
* Enable automated testing on Windows via AppVeyor * tests: add some tests for the command line argument parsing * cli: fixed windows argument parsing with escaped quotes, etc. * tests: python2.6 requires unittest2 * tests: fixed awful typo!
This commit is contained in:
parent
826dd87cc7
commit
f29b08c58c
|
@ -1,5 +1,4 @@
|
|||
language: python
|
||||
sudo: required
|
||||
|
||||
matrix:
|
||||
include:
|
||||
|
@ -11,8 +10,8 @@ matrix:
|
|||
env: BUILD_DOCS=yes BUILD_INSTALLER=yes STREAMLINK_INSTALLER_DIST_DIR=$TRAVIS_BUILD_DIR/dist/nsis
|
||||
|
||||
before_install:
|
||||
- pip install pytest pytest-cov codecov coverage mock pynsist
|
||||
- sudo pip install s3cmd
|
||||
- pip install --disable-pip-version-check --upgrade pip
|
||||
- pip install -r dev-requirements.txt
|
||||
|
||||
install:
|
||||
- python setup.py install
|
||||
|
|
|
@ -5,5 +5,6 @@ include LICENSE*
|
|||
include requirements-docs.txt
|
||||
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
recursive-include examples *
|
||||
recursive-include tests *py
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
environment:
|
||||
|
||||
matrix:
|
||||
- PYTHON: "C:\\Python27"
|
||||
- PYTHON: "C:\\Python33"
|
||||
- PYTHON: "C:\\Python34"
|
||||
- PYTHON: "C:\\Python35"
|
||||
- PYTHON: "C:\\Python27-x64"
|
||||
- PYTHON: "C:\\Python33-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
- PYTHON: "C:\\Python34-x64"
|
||||
DISTUTILS_USE_SDK: "1"
|
||||
- PYTHON: "C:\\Python35-x64"
|
||||
|
||||
install:
|
||||
# If there is a newer build queued for the same PR, cancel this one.
|
||||
# The AppVeyor 'rollout builds' option is supposed to serve the same
|
||||
# purpose but it is problematic because it tends to cancel builds pushed
|
||||
# directly to master instead of just PR builds (or the converse).
|
||||
# credits: JuliaLang developers.
|
||||
- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
|
||||
https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
|
||||
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
|
||||
throw "There are newer queued builds for this pull request, failing early." }
|
||||
|
||||
# Install Python (from the official .msi of http://python.org) and pip when
|
||||
# not already installed.
|
||||
- ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
|
||||
|
||||
# Prepend newly installed Python to the PATH of this build (this cannot be
|
||||
# done from inside the powershell script as it would require to restart
|
||||
# the parent CMD process).
|
||||
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
# Check that we have the expected version and architecture for Python
|
||||
- "python --version"
|
||||
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
|
||||
|
||||
# Upgrade to the latest version of pip to avoid it displaying warnings
|
||||
# about it being out of date.
|
||||
- "pip install --disable-pip-version-check --user --upgrade pip"
|
||||
|
||||
# install dev requirements, for testing, etc.
|
||||
- "pip install -r dev-requirements.txt"
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- "build.cmd %PYTHON%\\python.exe setup.py test"
|
|
@ -0,0 +1,21 @@
|
|||
@echo off
|
||||
:: To build extensions for 64 bit Python 3, we need to configure environment
|
||||
:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
|
||||
:: MS Windows SDK for Windows 7 and .NET Framework 4
|
||||
::
|
||||
:: More details at:
|
||||
:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
|
||||
|
||||
IF "%DISTUTILS_USE_SDK%"=="1" (
|
||||
ECHO Configuring environment to build with MSVC on a 64bit architecture
|
||||
ECHO Using Windows SDK 7.1
|
||||
"C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1
|
||||
CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release
|
||||
SET MSSdk=1
|
||||
REM Need the following to allow tox to see the SDK compiler
|
||||
SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB
|
||||
) ELSE (
|
||||
ECHO Using default MSVC build environment
|
||||
)
|
||||
|
||||
CALL %*
|
|
@ -0,0 +1,8 @@
|
|||
pip>=6
|
||||
pytest
|
||||
pytest-cov
|
||||
codecov
|
||||
coverage
|
||||
mock
|
||||
pynsist
|
||||
unittest2; python_version < '2.7'
|
|
@ -5,6 +5,8 @@ import sys
|
|||
|
||||
from time import sleep
|
||||
|
||||
import re
|
||||
|
||||
from .compat import is_win32, stdout
|
||||
from .constants import DEFAULT_PLAYER_ARGUMENTS
|
||||
from .utils import ignored
|
||||
|
@ -45,6 +47,7 @@ class Output(object):
|
|||
|
||||
class FileOutput(Output):
|
||||
def __init__(self, filename=None, fd=None):
|
||||
super(FileOutput, self).__init__()
|
||||
self.filename = filename
|
||||
self.fd = fd
|
||||
|
||||
|
@ -64,9 +67,9 @@ class FileOutput(Output):
|
|||
|
||||
|
||||
class PlayerOutput(Output):
|
||||
def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS,
|
||||
filename=None, quiet=True, kill=True,
|
||||
call=False, http=False, namedpipe=None):
|
||||
def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS, filename=None, quiet=True, kill=True, call=False, http=False,
|
||||
namedpipe=None):
|
||||
super(PlayerOutput, self).__init__()
|
||||
self.cmd = cmd
|
||||
self.args = args
|
||||
self.kill = kill
|
||||
|
@ -108,10 +111,7 @@ class PlayerOutput(Output):
|
|||
args = self.args.format(filename=filename)
|
||||
cmd = self.cmd
|
||||
if is_win32:
|
||||
# We want to keep the backslashes on Windows as forcing the user to
|
||||
# escape backslashes for paths would be inconvenient.
|
||||
cmd = cmd.replace("\\", "\\\\")
|
||||
args = args.replace("\\", "\\\\")
|
||||
return cmd + " " + args
|
||||
|
||||
return shlex.split(cmd) + shlex.split(args)
|
||||
|
||||
|
@ -139,7 +139,6 @@ class PlayerOutput(Output):
|
|||
stdin=self.stdin, bufsize=0,
|
||||
stdout=self.stdout,
|
||||
stderr=self.stderr)
|
||||
|
||||
# Wait 0.5 seconds to see if program exited prematurely
|
||||
if not self.running:
|
||||
raise OSError("Process exited prematurely")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from ..compat import shlex_quote
|
||||
import subprocess
|
||||
|
||||
|
||||
def check_paths(exes, paths):
|
||||
|
@ -35,5 +35,5 @@ def find_default_player():
|
|||
|
||||
if path:
|
||||
# Quote command because it can contain space
|
||||
return shlex_quote(path)
|
||||
return subprocess.list2cmdline([path])
|
||||
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
from io import BytesIO
|
||||
from itertools import repeat
|
||||
|
||||
from streamlink.plugins import Plugin
|
||||
from streamlink.options import Options
|
||||
from streamlink.stream import *
|
||||
|
||||
from streamlink.plugin.api.support_plugin import testplugin_support
|
||||
|
||||
|
||||
class TestStream(Stream):
|
||||
__shortname__ = "test"
|
||||
|
||||
def open(self):
|
||||
return BytesIO(b'x'*8192*2)
|
||||
|
||||
|
||||
class TestPlugin(Plugin):
|
||||
options = Options({
|
||||
"a_option": "default"
|
||||
|
@ -15,6 +26,7 @@ class TestPlugin(Plugin):
|
|||
|
||||
def _get_streams(self):
|
||||
streams = {}
|
||||
streams["test"] = TestStream(self.session)
|
||||
streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
|
||||
streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8")
|
||||
streams["http"] = HTTPStream(self.session, "http://test.se/stream")
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
import sys
|
||||
if sys.version_info[0:2] == (2, 6):
|
||||
import unittest2 as unittest
|
||||
else:
|
||||
import unittest
|
||||
|
||||
import os.path
|
||||
import streamlink_cli.main
|
||||
from mock import patch, ANY
|
||||
from streamlink import Streamlink
|
||||
from streamlink_cli.compat import is_win32
|
||||
|
||||
PluginPath = os.path.join(os.path.dirname(__file__), "plugins")
|
||||
|
||||
|
||||
def setup_streamlink():
|
||||
streamlink_cli.main.streamlink = Streamlink()
|
||||
streamlink_cli.main.streamlink.load_plugins(PluginPath)
|
||||
return streamlink_cli.main.streamlink
|
||||
|
||||
|
||||
class TestCommandLineInvocation(unittest.TestCase):
|
||||
"""
|
||||
Test that when invoked for the command line arguments are parsed as expected
|
||||
"""
|
||||
|
||||
@patch('streamlink_cli.main.setup_streamlink', side_effect=setup_streamlink)
|
||||
@patch('subprocess.Popen')
|
||||
@patch('sys.argv')
|
||||
def _test_args(self, args, commandline, mock_argv, mock_popen, mock_setup_streamlink, passthrough=False):
|
||||
mock_argv.__getitem__.side_effect = lambda x: args[x]
|
||||
mock_popen().returncode = None
|
||||
mock_popen().poll.return_value = None
|
||||
|
||||
streamlink_cli.main.main()
|
||||
mock_setup_streamlink.assert_called_with()
|
||||
if not passthrough:
|
||||
mock_popen.assert_called_with(commandline, stderr=ANY, stdout=ANY, bufsize = ANY, stdin = ANY)
|
||||
else:
|
||||
mock_popen.assert_called_with(commandline, stderr=ANY, stdout=ANY)
|
||||
|
||||
#
|
||||
# POSIX tests
|
||||
#
|
||||
|
||||
@unittest.skipIf(is_win32, "test only applicable in a POSIX OS")
|
||||
def test_open_regular_path_player(self):
|
||||
self._test_args(["streamlink", "-p", "/usr/bin/vlc", "http://test.se", "test"],
|
||||
["/usr/bin/vlc", "-"])
|
||||
|
||||
@unittest.skipIf(is_win32, "test only applicable in a POSIX OS")
|
||||
def test_open_space_path_player(self):
|
||||
self._test_args(["streamlink", "-p", "\"/Applications/Video Player/VLC/vlc\"", "http://test.se", "test"],
|
||||
["/Applications/Video Player/VLC/vlc", "-"])
|
||||
# escaped
|
||||
self._test_args(["streamlink", "-p", "/Applications/Video\ Player/VLC/vlc", "http://test.se", "test"],
|
||||
["/Applications/Video Player/VLC/vlc", "-"])
|
||||
|
||||
@unittest.skipIf(is_win32, "test only applicable in a POSIX OS")
|
||||
def test_open_player_extra_args_in_player(self):
|
||||
self._test_args(["streamlink", "-p", "/usr/bin/vlc",
|
||||
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
|
||||
"http://test.se", "test"],
|
||||
["/usr/bin/vlc", "--input-title-format", 'Poker "Stars"', "-"])
|
||||
|
||||
@unittest.skipIf(is_win32, "test only applicable in a POSIX OS")
|
||||
def test_open_player_extra_args_in_player_pass_through(self):
|
||||
self._test_args(["streamlink", "--player-passthrough", "rtmp", "-p", "/usr/bin/vlc",
|
||||
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
|
||||
"test.se", "rtmp"],
|
||||
["/usr/bin/vlc", "--input-title-format", 'Poker "Stars"', "rtmp://test.se"],
|
||||
passthrough=True)
|
||||
|
||||
#
|
||||
# Windows Tests
|
||||
#
|
||||
|
||||
@unittest.skipIf(not is_win32, "test only applicable on Windows")
|
||||
def test_open_space_path_player_win32(self):
|
||||
self._test_args(["streamlink", "-p", "c:\\Program Files\\VideoLAN\VLC\\vlc.exe", "http://test.se", "test"],
|
||||
"c:\\Program Files\\VideoLAN\\VLC\\vlc.exe -")
|
||||
|
||||
@unittest.skipIf(not is_win32, "test only applicable on Windows")
|
||||
def test_open_space_quote_path_player_win32(self):
|
||||
self._test_args(["streamlink", "-p", "\"c:\\Program Files\\VideoLAN\VLC\\vlc.exe\"", "http://test.se", "test"],
|
||||
"\"c:\\Program Files\\VideoLAN\\VLC\\vlc.exe\" -")
|
||||
|
||||
@unittest.skipIf(not is_win32, "test only applicable on Windows")
|
||||
def test_open_player_args_with_quote_in_player_win32(self):
|
||||
self._test_args(["streamlink", "-p",
|
||||
'''c:\\Program Files\\VideoLAN\VLC\\vlc.exe --input-title-format "Poker \\"Stars\\""''',
|
||||
"http://test.se", "test"],
|
||||
'''c:\\Program Files\\VideoLAN\VLC\\vlc.exe --input-title-format "Poker \\"Stars\\"" -''')
|
||||
|
||||
@unittest.skipIf(not is_win32, "test only applicable on Windows")
|
||||
def test_open_player_extra_args_in_player_win32(self):
|
||||
self._test_args(["streamlink", "-p", "c:\\Program Files\\VideoLAN\VLC\\vlc.exe",
|
||||
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
|
||||
"http://test.se", "test"],
|
||||
'''c:\\Program Files\\VideoLAN\VLC\\vlc.exe --input-title-format "Poker \\"Stars\\"" -''')
|
||||
|
||||
@unittest.skipIf(not is_win32, "test only applicable on Windows")
|
||||
def test_open_player_extra_args_in_player_pass_through_win32(self):
|
||||
self._test_args(["streamlink", "--player-passthrough", "rtmp", "-p", "c:\\Program Files\\VideoLAN\VLC\\vlc.exe",
|
||||
"-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''',
|
||||
"test.se", "rtmp"],
|
||||
'''c:\\Program Files\\VideoLAN\VLC\\vlc.exe --input-title-format "Poker \\"Stars\\"" \"rtmp://test.se\"''',
|
||||
passthrough=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -13,7 +13,9 @@ from streamlink.plugin.api.http_session import HTTPSession
|
|||
|
||||
|
||||
class TestPluginAPIHTTPSession(unittest.TestCase):
|
||||
def test_read_timeout(self):
|
||||
@patch('requests.sessions.Session.send')
|
||||
def test_read_timeout(self, mock_send):
|
||||
mock_send.side_effect = IOError
|
||||
session = HTTPSession()
|
||||
|
||||
def stream_data():
|
||||
|
|
Loading…
Reference in New Issue