Compare commits

..

No commits in common. "master" and "v2.0.0" have entirely different histories.

5 changed files with 94 additions and 285 deletions

View File

@ -1,6 +1,4 @@
language: python language: python
sudo: required
dist: xenial
addons: addons:
apt: apt:
@ -35,13 +33,8 @@ matrix:
env: TOXENV=py35 env: TOXENV=py35
- python: 3.6 - python: 3.6
env: TOXENV=py36 env: TOXENV=py36
- python: 3.7
env: TOXENV=py37
- python: 3.8-dev
env: TOXENV=py38
- python: pypy - python: pypy
env: TOXENV=pypy env: TOXENV=pypy
dist: trusty
before_install: before_install:
- if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi; - if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi;
@ -49,10 +42,7 @@ before_install:
install: install:
- if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi; - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py26|py33)") != 0 ]]; then pip install virtualenv==15.2.0 tox==2.9.1; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi;
- if [[ $(echo "$TOXENV" | egrep -c "(py2[456]|py3[123])") == 0 ]]; then pip install tox; fi;
script: script:
- tox - tox

View File

@ -51,8 +51,7 @@ or
:: ::
git clone https://github.com/sivel/speedtest-cli.git git clone https://github.com/sivel/speedtest-cli.git
cd speedtest-cli python speedtest-cli/setup.py install
python setup.py install
Just download (Like the way it used to be) Just download (Like the way it used to be)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -75,23 +74,21 @@ Usage
:: ::
$ speedtest-cli -h $ speedtest-cli -h
usage: speedtest-cli [-h] [--no-download] [--no-upload] [--single] [--bytes] usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share]
[--share] [--simple] [--csv] [--simple] [--csv] [--csv-delimiter CSV_DELIMITER]
[--csv-delimiter CSV_DELIMITER] [--csv-header] [--json] [--csv-header] [--json] [--list] [--server SERVER]
[--list] [--server SERVER] [--exclude EXCLUDE] [--exclude EXCLUDE] [--mini MINI] [--source SOURCE]
[--mini MINI] [--source SOURCE] [--timeout TIMEOUT] [--timeout TIMEOUT] [--secure] [--no-pre-allocate]
[--secure] [--no-pre-allocate] [--version] [--version]
Command line interface for testing internet bandwidth using speedtest.net. Command line interface for testing internet bandwidth using speedtest.net.
-------------------------------------------------------------------------- --------------------------------------------------------------------------
https://github.com/sivel/speedtest-cli https://github.com/sivel/speedtest-cli
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
--no-download Do not perform download test --no-download Do not perform download test
--no-upload Do not perform upload test --no-upload Do not perform upload test
--single Only use a single connection instead of multiple. This
simulates a typical file transfer.
--bytes Display values in bytes instead of bits. Does not --bytes Display values in bytes instead of bits. Does not
affect the image generated by --share, nor output from affect the image generated by --share, nor output from
--json or --csv --json or --csv

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012 Matt Martz # Copyright 2012-2018 Matt Martz
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -92,8 +92,5 @@ setup(
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
] ]
) )

View File

@ -33,11 +33,6 @@ Do not perform download test
Do not perform upload test Do not perform upload test
.RE .RE
\fB\-\-single\fR
.RS
Only use a single connection instead of multiple. This simulates a typical file transfer.
.RE
\fB\-\-bytes\fR \fB\-\-bytes\fR
.RS .RS
Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2012 Matt Martz # Copyright 2012-2018 Matt Martz
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -15,18 +15,18 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import csv
import datetime
import errno
import math
import os import os
import platform
import re import re
import csv
import sys
import math
import errno
import signal import signal
import socket import socket
import sys
import threading
import timeit import timeit
import datetime
import platform
import threading
import xml.parsers.expat import xml.parsers.expat
try: try:
@ -36,7 +36,7 @@ except ImportError:
gzip = None gzip = None
GZIP_BASE = object GZIP_BASE = object
__version__ = '2.1.4b1' __version__ = '2.0.0'
class FakeShutdownEvent(object): class FakeShutdownEvent(object):
@ -49,16 +49,10 @@ class FakeShutdownEvent(object):
"Dummy method to always return false""" "Dummy method to always return false"""
return False return False
is_set = isSet
# Some global variables we use # Some global variables we use
DEBUG = False DEBUG = False
_GLOBAL_DEFAULT_TIMEOUT = object() _GLOBAL_DEFAULT_TIMEOUT = object()
PY25PLUS = sys.version_info[:2] >= (2, 5)
PY26PLUS = sys.version_info[:2] >= (2, 6)
PY32PLUS = sys.version_info[:2] >= (3, 2)
PY310PLUS = sys.version_info[:2] >= (3, 10)
# Begin import game to handle Python 2 and Python 3 # Begin import game to handle Python 2 and Python 3
try: try:
@ -70,15 +64,13 @@ except ImportError:
json = None json = None
try: try:
import xml.etree.ElementTree as ET import xml.etree.cElementTree as ET
try:
from xml.etree.ElementTree import _Element as ET_Element
except ImportError:
pass
except ImportError: except ImportError:
from xml.dom import minidom as DOM try:
from xml.parsers.expat import ExpatError import xml.etree.ElementTree as ET
ET = None except ImportError:
from xml.dom import minidom as DOM
ET = None
try: try:
from urllib2 import (urlopen, Request, HTTPError, URLError, from urllib2 import (urlopen, Request, HTTPError, URLError,
@ -92,9 +84,9 @@ except ImportError:
HTTPErrorProcessor, OpenerDirector) HTTPErrorProcessor, OpenerDirector)
try: try:
from httplib import HTTPConnection, BadStatusLine from httplib import HTTPConnection
except ImportError: except ImportError:
from http.client import HTTPConnection, BadStatusLine from http.client import HTTPConnection
try: try:
from httplib import HTTPSConnection from httplib import HTTPSConnection
@ -104,11 +96,6 @@ except ImportError:
except ImportError: except ImportError:
HTTPSConnection = None HTTPSConnection = None
try:
from httplib import FakeSocket
except ImportError:
FakeSocket = None
try: try:
from Queue import Queue from Queue import Queue
except ImportError: except ImportError:
@ -269,6 +256,7 @@ else:
write(arg) write(arg)
write(end) write(end)
# Exception "constants" to support Python 2 through Python 3 # Exception "constants" to support Python 2 through Python 3
try: try:
import ssl import ssl
@ -277,30 +265,10 @@ try:
except AttributeError: except AttributeError:
CERT_ERROR = tuple() CERT_ERROR = tuple()
HTTP_ERRORS = ( HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) +
(HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) + CERT_ERROR)
CERT_ERROR
)
except ImportError: except ImportError:
ssl = None HTTP_ERRORS = (HTTPError, URLError, socket.error)
HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine)
if PY32PLUS:
etree_iter = ET.Element.iter
elif PY25PLUS:
etree_iter = ET_Element.getiterator
if PY26PLUS:
thread_is_alive = threading.Thread.is_alive
else:
thread_is_alive = threading.Thread.isAlive
def event_is_set(event):
try:
return event.is_set()
except AttributeError:
return event.isSet()
class SpeedtestException(Exception): class SpeedtestException(Exception):
@ -316,11 +284,7 @@ class SpeedtestHTTPError(SpeedtestException):
class SpeedtestConfigError(SpeedtestException): class SpeedtestConfigError(SpeedtestException):
"""Configuration XML is invalid""" """Configuration provided is invalid"""
class SpeedtestServersError(SpeedtestException):
"""Servers XML is invalid"""
class ConfigRetrievalError(SpeedtestHTTPError): class ConfigRetrievalError(SpeedtestHTTPError):
@ -420,13 +384,13 @@ class SpeedtestHTTPConnection(HTTPConnection):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
source_address = kwargs.pop('source_address', None) source_address = kwargs.pop('source_address', None)
context = kwargs.pop('context', None)
timeout = kwargs.pop('timeout', 10) timeout = kwargs.pop('timeout', 10)
self._tunnel_host = None
HTTPConnection.__init__(self, *args, **kwargs) HTTPConnection.__init__(self, *args, **kwargs)
self.source_address = source_address self.source_address = source_address
self._context = context
self.timeout = timeout self.timeout = timeout
def connect(self): def connect(self):
@ -444,75 +408,23 @@ class SpeedtestHTTPConnection(HTTPConnection):
self.source_address self.source_address
) )
if self._tunnel_host:
self._tunnel()
if HTTPSConnection: if HTTPSConnection:
class SpeedtestHTTPSConnection(HTTPSConnection): class SpeedtestHTTPSConnection(HTTPSConnection,
SpeedtestHTTPConnection):
"""Custom HTTPSConnection to support source_address across """Custom HTTPSConnection to support source_address across
Python 2.4 - Python 3 Python 2.4 - Python 3
""" """
default_port = 443
def __init__(self, *args, **kwargs):
source_address = kwargs.pop('source_address', None)
timeout = kwargs.pop('timeout', 10)
self._tunnel_host = None
HTTPSConnection.__init__(self, *args, **kwargs)
self.timeout = timeout
self.source_address = source_address
def connect(self): def connect(self):
"Connect to a host on a given (SSL) port." "Connect to a host on a given (SSL) port."
try:
self.sock = socket.create_connection(
(self.host, self.port),
self.timeout,
self.source_address
)
except (AttributeError, TypeError):
self.sock = create_connection(
(self.host, self.port),
self.timeout,
self.source_address
)
if self._tunnel_host: SpeedtestHTTPConnection.connect(self)
self._tunnel()
if ssl: kwargs = {}
try: if hasattr(ssl, 'SSLContext'):
kwargs = {} kwargs['server_hostname'] = self.host
if hasattr(ssl, 'SSLContext'):
if self._tunnel_host: self.sock = self._context.wrap_socket(self.sock, **kwargs)
kwargs['server_hostname'] = self._tunnel_host
else:
kwargs['server_hostname'] = self.host
self.sock = self._context.wrap_socket(self.sock, **kwargs)
except AttributeError:
self.sock = ssl.wrap_socket(self.sock)
try:
self.sock.server_hostname = self.host
except AttributeError:
pass
elif FakeSocket:
# Python 2.4/2.5 support
try:
self.sock = FakeSocket(self.sock, socket.ssl(self.sock))
except AttributeError:
raise SpeedtestException(
'This version of Python does not support HTTPS/SSL '
'functionality'
)
else:
raise SpeedtestException(
'This version of Python does not support HTTPS/SSL '
'functionality'
)
def _build_connection(connection, source_address, timeout, context=None): def _build_connection(connection, source_address, timeout, context=None):
@ -677,8 +589,7 @@ def build_user_agent():
ua_tuple = ( ua_tuple = (
'Mozilla/5.0', 'Mozilla/5.0',
'(%s; U; %s; en-us)' % (platform.platform(), '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]),
platform.architecture()[0]),
'Python/%s' % platform.python_version(), 'Python/%s' % platform.python_version(),
'(KHTML, like Gecko)', '(KHTML, like Gecko)',
'speedtest-cli/%s' % __version__ 'speedtest-cli/%s' % __version__
@ -737,8 +648,6 @@ def catch_request(request, opener=None):
try: try:
uh = _open(request) uh = _open(request)
if request.get_full_url() != uh.geturl():
printer('Redirected to %s' % uh.geturl(), debug=True)
return uh, False return uh, False
except HTTP_ERRORS: except HTTP_ERRORS:
e = get_exception() e = get_exception()
@ -778,7 +687,7 @@ def print_dots(shutdown_event):
status status
""" """
def inner(current, total, start=False, end=False): def inner(current, total, start=False, end=False):
if event_is_set(shutdown_event): if shutdown_event.isSet():
return return
sys.stdout.write('.') sys.stdout.write('.')
@ -817,7 +726,7 @@ class HTTPDownloader(threading.Thread):
try: try:
if (timeit.default_timer() - self.starttime) <= self.timeout: if (timeit.default_timer() - self.starttime) <= self.timeout:
f = self._opener(self.request) f = self._opener(self.request)
while (not event_is_set(self._shutdown_event) and while (not self._shutdown_event.isSet() and
(timeit.default_timer() - self.starttime) <= (timeit.default_timer() - self.starttime) <=
self.timeout): self.timeout):
self.result.append(len(f.read(10240))) self.result.append(len(f.read(10240)))
@ -826,8 +735,6 @@ class HTTPDownloader(threading.Thread):
f.close() f.close()
except IOError: except IOError:
pass pass
except HTTP_ERRORS:
pass
class HTTPUploaderData(object): class HTTPUploaderData(object):
@ -873,7 +780,7 @@ class HTTPUploaderData(object):
def read(self, n=10240): def read(self, n=10240):
if ((timeit.default_timer() - self.start) <= self.timeout and if ((timeit.default_timer() - self.start) <= self.timeout and
not event_is_set(self._shutdown_event)): not self._shutdown_event.isSet()):
chunk = self.data.read(n) chunk = self.data.read(n)
self.total.append(len(chunk)) self.total.append(len(chunk))
return chunk return chunk
@ -893,7 +800,7 @@ class HTTPUploader(threading.Thread):
self.request = request self.request = request
self.request.data.start = self.starttime = start self.request.data.start = self.starttime = start
self.size = size self.size = size
self.result = 0 self.result = None
self.timeout = timeout self.timeout = timeout
self.i = i self.i = i
@ -911,7 +818,7 @@ class HTTPUploader(threading.Thread):
request = self.request request = self.request
try: try:
if ((timeit.default_timer() - self.starttime) <= self.timeout and if ((timeit.default_timer() - self.starttime) <= self.timeout and
not event_is_set(self._shutdown_event)): not self._shutdown_event.isSet()):
try: try:
f = self._opener(request) f = self._opener(request)
except TypeError: except TypeError:
@ -928,8 +835,6 @@ class HTTPUploader(threading.Thread):
self.result = 0 self.result = 0
except (IOError, SpeedtestUploadTimeout): except (IOError, SpeedtestUploadTimeout):
self.result = sum(self.request.data.total) self.result = sum(self.request.data.total)
except HTTP_ERRORS:
self.result = 0
class SpeedtestResults(object): class SpeedtestResults(object):
@ -1118,7 +1023,10 @@ class Speedtest(object):
@property @property
def best(self): def best(self):
if not self._best: if not self._best:
self.get_best_server() raise SpeedtestMissingBestServer(
'get_best_server not called or not able to determine best '
'server'
)
return self._best return self._best
def get_config(self): def get_config(self):
@ -1134,16 +1042,16 @@ class Speedtest(object):
uh, e = catch_request(request, opener=self._opener) uh, e = catch_request(request, opener=self._opener)
if e: if e:
raise ConfigRetrievalError(e) raise ConfigRetrievalError(e)
configxml_list = [] configxml = []
stream = get_response_stream(uh) stream = get_response_stream(uh)
while 1: while 1:
try: try:
configxml_list.append(stream.read(1024)) configxml.append(stream.read(1024))
except (OSError, EOFError): except (OSError, EOFError):
raise ConfigRetrievalError(get_exception()) raise ConfigRetrievalError(get_exception())
if len(configxml_list[-1]) == 0: if len(configxml[-1]) == 0:
break break
stream.close() stream.close()
uh.close() uh.close()
@ -1151,18 +1059,10 @@ class Speedtest(object):
if int(uh.code) != 200: if int(uh.code) != 200:
return None return None
configxml = ''.encode().join(configxml_list) printer('Config XML:\n%s' % ''.encode().join(configxml), debug=True)
printer('Config XML:\n%s' % configxml, debug=True)
try: try:
try: root = ET.fromstring(''.encode().join(configxml))
root = ET.fromstring(configxml)
except ET.ParseError:
e = get_exception()
raise SpeedtestConfigError(
'Malformed speedtest.net configuration: %s' % e
)
server_config = root.find('server-config').attrib server_config = root.find('server-config').attrib
download = root.find('download').attrib download = root.find('download').attrib
upload = root.find('upload').attrib upload = root.find('upload').attrib
@ -1170,22 +1070,16 @@ class Speedtest(object):
client = root.find('client').attrib client = root.find('client').attrib
except AttributeError: except AttributeError:
try: root = DOM.parseString(''.join(configxml))
root = DOM.parseString(configxml)
except ExpatError:
e = get_exception()
raise SpeedtestConfigError(
'Malformed speedtest.net configuration: %s' % e
)
server_config = get_attributes_by_tag_name(root, 'server-config') server_config = get_attributes_by_tag_name(root, 'server-config')
download = get_attributes_by_tag_name(root, 'download') download = get_attributes_by_tag_name(root, 'download')
upload = get_attributes_by_tag_name(root, 'upload') upload = get_attributes_by_tag_name(root, 'upload')
# times = get_attributes_by_tag_name(root, 'times') # times = get_attributes_by_tag_name(root, 'times')
client = get_attributes_by_tag_name(root, 'client') client = get_attributes_by_tag_name(root, 'client')
ignore_servers = [ ignore_servers = list(
int(i) for i in server_config['ignoreids'].split(',') if i map(int, server_config['ignoreids'].split(','))
] )
ratio = int(upload['ratio']) ratio = int(upload['ratio'])
upload_max = int(upload['maxchunkcount']) upload_max = int(upload['maxchunkcount'])
@ -1225,13 +1119,7 @@ class Speedtest(object):
'upload_max': upload_count * size_count 'upload_max': upload_count * size_count
}) })
try: self.lat_lon = (float(client['lat']), float(client['lon']))
self.lat_lon = (float(client['lat']), float(client['lon']))
except ValueError:
raise SpeedtestConfigError(
'Unknown location: lat=%r lon=%r' %
(client.get('lat'), client.get('lon'))
)
printer('Config:\n%r' % self.config, debug=True) printer('Config:\n%r' % self.config, debug=True)
@ -1285,13 +1173,13 @@ class Speedtest(object):
stream = get_response_stream(uh) stream = get_response_stream(uh)
serversxml_list = [] serversxml = []
while 1: while 1:
try: try:
serversxml_list.append(stream.read(1024)) serversxml.append(stream.read(1024))
except (OSError, EOFError): except (OSError, EOFError):
raise ServersRetrievalError(get_exception()) raise ServersRetrievalError(get_exception())
if len(serversxml_list[-1]) == 0: if len(serversxml[-1]) == 0:
break break
stream.close() stream.close()
@ -1300,28 +1188,15 @@ class Speedtest(object):
if int(uh.code) != 200: if int(uh.code) != 200:
raise ServersRetrievalError() raise ServersRetrievalError()
serversxml = ''.encode().join(serversxml_list) printer('Servers XML:\n%s' % ''.encode().join(serversxml),
debug=True)
printer('Servers XML:\n%s' % serversxml, debug=True)
try: try:
try: try:
try: root = ET.fromstring(''.encode().join(serversxml))
root = ET.fromstring(serversxml) elements = root.getiterator('server')
except ET.ParseError:
e = get_exception()
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = etree_iter(root, 'server')
except AttributeError: except AttributeError:
try: root = DOM.parseString(''.join(serversxml))
root = DOM.parseString(serversxml)
except ExpatError:
e = get_exception()
raise SpeedtestServersError(
'Malformed speedtest.net server list: %s' % e
)
elements = root.getElementsByTagName('server') elements = root.getElementsByTagName('server')
except (SyntaxError, xml.parsers.expat.ExpatError): except (SyntaxError, xml.parsers.expat.ExpatError):
raise ServersRetrievalError() raise ServersRetrievalError()
@ -1513,12 +1388,8 @@ class Speedtest(object):
printer('Best Server:\n%r' % best, debug=True) printer('Best Server:\n%r' % best, debug=True)
return best return best
def download(self, callback=do_nothing, threads=None): def download(self, callback=do_nothing):
"""Test download speed against speedtest.net """Test download speed against speedtest.net"""
A ``threads`` value of ``None`` will fall back to those dictated
by the speedtest.net configuration
"""
urls = [] urls = []
for size in self.config['sizes']['download']: for size in self.config['sizes']['download']:
@ -1533,9 +1404,6 @@ class Speedtest(object):
build_request(url, bump=i, secure=self._secure) build_request(url, bump=i, secure=self._secure)
) )
max_threads = threads or self.config['threads']['download']
in_flight = {'threads': 0}
def producer(q, requests, request_count): def producer(q, requests, request_count):
for i, request in enumerate(requests): for i, request in enumerate(requests):
thread = HTTPDownloader( thread = HTTPDownloader(
@ -1546,26 +1414,21 @@ class Speedtest(object):
opener=self._opener, opener=self._opener,
shutdown_event=self._shutdown_event shutdown_event=self._shutdown_event
) )
while in_flight['threads'] >= max_threads:
timeit.time.sleep(0.001)
thread.start() thread.start()
q.put(thread, True) q.put(thread, True)
in_flight['threads'] += 1
callback(i, request_count, start=True) callback(i, request_count, start=True)
finished = [] finished = []
def consumer(q, request_count): def consumer(q, request_count):
_is_alive = thread_is_alive
while len(finished) < request_count: while len(finished) < request_count:
thread = q.get(True) thread = q.get(True)
while _is_alive(thread): while thread.isAlive():
thread.join(timeout=0.001) thread.join(timeout=0.1)
in_flight['threads'] -= 1
finished.append(sum(thread.result)) finished.append(sum(thread.result))
callback(thread.i, request_count, end=True) callback(thread.i, request_count, end=True)
q = Queue(max_threads) q = Queue(self.config['threads']['download'])
prod_thread = threading.Thread(target=producer, prod_thread = threading.Thread(target=producer,
args=(q, requests, request_count)) args=(q, requests, request_count))
cons_thread = threading.Thread(target=consumer, cons_thread = threading.Thread(target=consumer,
@ -1573,11 +1436,10 @@ class Speedtest(object):
start = timeit.default_timer() start = timeit.default_timer()
prod_thread.start() prod_thread.start()
cons_thread.start() cons_thread.start()
_is_alive = thread_is_alive while prod_thread.isAlive():
while _is_alive(prod_thread): prod_thread.join(timeout=0.1)
prod_thread.join(timeout=0.001) while cons_thread.isAlive():
while _is_alive(cons_thread): cons_thread.join(timeout=0.1)
cons_thread.join(timeout=0.001)
stop = timeit.default_timer() stop = timeit.default_timer()
self.results.bytes_received = sum(finished) self.results.bytes_received = sum(finished)
@ -1588,12 +1450,8 @@ class Speedtest(object):
self.config['threads']['upload'] = 8 self.config['threads']['upload'] = 8
return self.results.download return self.results.download
def upload(self, callback=do_nothing, pre_allocate=True, threads=None): def upload(self, callback=do_nothing, pre_allocate=True):
"""Test upload speed against speedtest.net """Test upload speed against speedtest.net"""
A ``threads`` value of ``None`` will fall back to those dictated
by the speedtest.net configuration
"""
sizes = [] sizes = []
@ -1616,19 +1474,13 @@ class Speedtest(object):
) )
if pre_allocate: if pre_allocate:
data.pre_allocate() data.pre_allocate()
headers = {'Content-length': size}
requests.append( requests.append(
( (
build_request(self.best['url'], data, secure=self._secure, build_request(self.best['url'], data, secure=self._secure),
headers=headers),
size size
) )
) )
max_threads = threads or self.config['threads']['upload']
in_flight = {'threads': 0}
def producer(q, requests, request_count): def producer(q, requests, request_count):
for i, request in enumerate(requests[:request_count]): for i, request in enumerate(requests[:request_count]):
thread = HTTPUploader( thread = HTTPUploader(
@ -1640,26 +1492,21 @@ class Speedtest(object):
opener=self._opener, opener=self._opener,
shutdown_event=self._shutdown_event shutdown_event=self._shutdown_event
) )
while in_flight['threads'] >= max_threads:
timeit.time.sleep(0.001)
thread.start() thread.start()
q.put(thread, True) q.put(thread, True)
in_flight['threads'] += 1
callback(i, request_count, start=True) callback(i, request_count, start=True)
finished = [] finished = []
def consumer(q, request_count): def consumer(q, request_count):
_is_alive = thread_is_alive
while len(finished) < request_count: while len(finished) < request_count:
thread = q.get(True) thread = q.get(True)
while _is_alive(thread): while thread.isAlive():
thread.join(timeout=0.001) thread.join(timeout=0.1)
in_flight['threads'] -= 1
finished.append(thread.result) finished.append(thread.result)
callback(thread.i, request_count, end=True) callback(thread.i, request_count, end=True)
q = Queue(threads or self.config['threads']['upload']) q = Queue(self.config['threads']['upload'])
prod_thread = threading.Thread(target=producer, prod_thread = threading.Thread(target=producer,
args=(q, requests, request_count)) args=(q, requests, request_count))
cons_thread = threading.Thread(target=consumer, cons_thread = threading.Thread(target=consumer,
@ -1667,10 +1514,9 @@ class Speedtest(object):
start = timeit.default_timer() start = timeit.default_timer()
prod_thread.start() prod_thread.start()
cons_thread.start() cons_thread.start()
_is_alive = thread_is_alive while prod_thread.isAlive():
while _is_alive(prod_thread):
prod_thread.join(timeout=0.1) prod_thread.join(timeout=0.1)
while _is_alive(cons_thread): while cons_thread.isAlive():
cons_thread.join(timeout=0.1) cons_thread.join(timeout=0.1)
stop = timeit.default_timer() stop = timeit.default_timer()
@ -1695,8 +1541,7 @@ def ctrl_c(shutdown_event):
def version(): def version():
"""Print the version""" """Print the version"""
printer('speedtest-cli %s' % __version__) printer(__version__)
printer('Python %s' % sys.version.replace('\n', ''))
sys.exit(0) sys.exit(0)
@ -1729,10 +1574,6 @@ def parse_args():
parser.add_argument('--no-upload', dest='upload', default=True, parser.add_argument('--no-upload', dest='upload', default=True,
action='store_const', const=False, action='store_const', const=False,
help='Do not perform upload test') help='Do not perform upload test')
parser.add_argument('--single', default=False, action='store_true',
help='Only use a single connection instead of '
'multiple. This simulates a typical file '
'transfer.')
parser.add_argument('--bytes', dest='units', action='store_const', parser.add_argument('--bytes', dest='units', action='store_const',
const=('byte', 8), default=('bit', 1), const=('byte', 8), default=('bit', 1),
help='Display values in bytes instead of bits. Does ' help='Display values in bytes instead of bits. Does '
@ -1947,10 +1788,7 @@ def shell():
if args.download: if args.download:
printer('Testing download speed', quiet, printer('Testing download speed', quiet,
end=('', '\n')[bool(debug)]) end=('', '\n')[bool(debug)])
speedtest.download( speedtest.download(callback=callback)
callback=callback,
threads=(None, 1)[args.single]
)
printer('Download: %0.2f M%s/s' % printer('Download: %0.2f M%s/s' %
((results.download / 1000.0 / 1000.0) / args.units[1], ((results.download / 1000.0 / 1000.0) / args.units[1],
args.units[0]), args.units[0]),
@ -1961,11 +1799,7 @@ def shell():
if args.upload: if args.upload:
printer('Testing upload speed', quiet, printer('Testing upload speed', quiet,
end=('', '\n')[bool(debug)]) end=('', '\n')[bool(debug)])
speedtest.upload( speedtest.upload(callback=callback, pre_allocate=args.pre_allocate)
callback=callback,
pre_allocate=args.pre_allocate,
threads=(None, 1)[args.single]
)
printer('Upload: %0.2f M%s/s' % printer('Upload: %0.2f M%s/s' %
((results.upload / 1000.0 / 1000.0) / args.units[1], ((results.upload / 1000.0 / 1000.0) / args.units[1],
args.units[0]), args.units[0]),
@ -1975,9 +1809,6 @@ def shell():
printer('Results:\n%r' % results.dict(), debug=True) printer('Results:\n%r' % results.dict(), debug=True)
if not args.simple and args.share:
results.share()
if args.simple: if args.simple:
printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' %
(results.ping, (results.ping,
@ -1988,6 +1819,8 @@ def shell():
elif args.csv: elif args.csv:
printer(results.csv(delimiter=args.csv_delimiter)) printer(results.csv(delimiter=args.csv_delimiter))
elif args.json: elif args.json:
if args.share:
results.share()
printer(results.json()) printer(results.json())
if args.share and not machine_format: if args.share and not machine_format:
@ -2003,10 +1836,7 @@ def main():
e = get_exception() e = get_exception()
# Ignore a successful exit, or argparse exit # Ignore a successful exit, or argparse exit
if getattr(e, 'code', 1) not in (0, 2): if getattr(e, 'code', 1) not in (0, 2):
msg = '%s' % e raise SystemExit('ERROR: %s' % e)
if not msg:
msg = '%r' % e
raise SystemExit('ERROR: %s' % msg)
if __name__ == '__main__': if __name__ == '__main__':