Skip to content

Commit

Permalink
Showing 22 changed files with 144 additions and 122 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -12,3 +12,4 @@ dist/**
.vagrant
build/
.coverage
.tox/
22 changes: 16 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
# command to install dependencies
# Workaround for https://github.com/travis-ci/travis-ci/issues/4794
- 3.5
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
addons:
apt:
packages:
- libevent-dev
install:
- sudo apt-get install -y libevent-dev
# command to run tests
script: python setup.py test
- pip install tox
script:
- tox
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
test:
python setup.py test
unit2 discover

release:
python setup.py sdist upload
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -62,5 +62,5 @@ Open source licensed under the MIT license (see _LICENSE_ file for details).

## Supported Python Versions

Locust requires **Python 2.6+**. It is not currently compatible with Python 3.x.
Locust supports Python 2.6, 2.7 and 3.4.

4 changes: 2 additions & 2 deletions locust/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core import HttpLocust, Locust, TaskSet, task
from exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately
from .core import HttpLocust, Locust, TaskSet, task
from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately

__version__ = "0.7.5"
9 changes: 5 additions & 4 deletions locust/clients.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import re
import time
from datetime import timedelta
from urlparse import urlparse, urlunparse
from six.moves.urllib.parse import urlparse, urlunparse
import six

import requests
from requests import Response, Request
from requests.auth import HTTPBasicAuth
from requests.exceptions import (RequestException, MissingSchema,
InvalidSchema, InvalidURL)

import events
from exception import CatchResponseError, ResponseError
from . import events
from .exception import CatchResponseError, ResponseError

absolute_http_url_regexp = re.compile(r"^https?://", re.I)

@@ -235,7 +236,7 @@ def failure(self, exc):
if response.content == "":
response.failure("No data")
"""
if isinstance(exc, basestring):
if isinstance(exc, six.string_types):
exc = CatchResponseError(exc)

events.request_failure.fire(
27 changes: 14 additions & 13 deletions locust/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import gevent
from gevent import monkey, GreenletExit
import six
from six.moves import xrange

monkey.patch_all(thread=False)

@@ -10,10 +12,10 @@
import traceback
import logging

from clients import HttpSession
import events
from .clients import HttpSession
from . import events

from exception import LocustError, InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopLocust
from .exception import LocustError, InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopLocust

logger = logging.getLogger(__name__)

@@ -103,7 +105,7 @@ def run(self):
except StopLocust:
pass
except (RescheduleTask, RescheduleTaskImmediately) as e:
raise LocustError, LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)), sys.exc_info()[2]
six.reraise(LocustError, LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)), sys.exc_info()[2])


class HttpLocust(Locust):
@@ -146,7 +148,7 @@ def __new__(mcs, classname, bases, classDict):
if "tasks" in classDict and classDict["tasks"] is not None:
tasks = classDict["tasks"]
if isinstance(tasks, dict):
tasks = list(tasks.iteritems())
tasks = six.iteritems(tasks)

for task in tasks:
if isinstance(task, tuple):
@@ -156,7 +158,7 @@ def __new__(mcs, classname, bases, classDict):
else:
new_tasks.append(task)

for item in classDict.itervalues():
for item in six.itervalues(classDict):
if hasattr(item, "locust_task_weight"):
for i in xrange(0, item.locust_task_weight):
new_tasks.append(item)
@@ -165,6 +167,7 @@ def __new__(mcs, classname, bases, classDict):

return type.__new__(mcs, classname, bases, classDict)

@six.add_metaclass(TaskSetMeta)
class TaskSet(object):
"""
Class defining a set of tasks that a Locust user will execute.
@@ -221,8 +224,6 @@ class ForumPage(TaskSet):
instantiated. Useful for nested TaskSet classes.
"""

__metaclass__ = TaskSetMeta

def __init__(self, parent):
self._task_queue = []
self._time_start = time()
@@ -251,9 +252,9 @@ def run(self, *args, **kwargs):
self.on_start()
except InterruptTaskSet as e:
if e.reschedule:
raise RescheduleTaskImmediately, e, sys.exc_info()[2]
six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
else:
raise RescheduleTask, e, sys.exc_info()[2]
six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])

while (True):
try:
@@ -273,9 +274,9 @@ def run(self, *args, **kwargs):
self.wait()
except InterruptTaskSet as e:
if e.reschedule:
raise RescheduleTaskImmediately, e, sys.exc_info()[2]
six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
else:
raise RescheduleTask, e, sys.exc_info()[2]
six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])
except StopLocust:
raise
except GreenletExit:
@@ -294,7 +295,7 @@ def execute_next_task(self):

def execute_task(self, task, *args, **kwargs):
# check if the function is a method bound to the current locust, and if so, don't pass self as first argument
if hasattr(task, "im_self") and task.__self__ == self:
if hasattr(task, "__self__") and task.__self__ == self:
# task is a bound method on self
task(*args, **kwargs)
elif hasattr(task, "tasks") and issubclass(task, TaskSet):
13 changes: 7 additions & 6 deletions locust/inspectlocust.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import inspect
import six

from core import Locust, TaskSet
from log import console_logger
from .core import Locust, TaskSet
from .log import console_logger

def print_task_ratio(locusts, total=False, level=0, parent_ratio=1.0):
d = get_task_ratio_dict(locusts, total=total, parent_ratio=parent_ratio)
_print_task_ratio(d)

def _print_task_ratio(x, level=0):
for k, v in x.iteritems():
for k, v in six.iteritems(x):
padding = 2*" "*level
ratio = v.get('ratio', 1)
console_logger.info(" %-10s %-50s" % (padding + "%-6.1f" % (ratio*100), padding + k))
@@ -30,10 +31,10 @@ def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0):
ratio[task] += task.weight if hasattr(task, 'weight') else 1

# get percentage
ratio_percent = dict((k, float(v) / divisor) for k, v in ratio.iteritems())
ratio_percent = dict((k, float(v) / divisor) for k, v in six.iteritems(ratio))

task_dict = {}
for locust, ratio in ratio_percent.iteritems():
for locust, ratio in six.iteritems(ratio_percent):
d = {"ratio":ratio}
if inspect.isclass(locust):
if issubclass(locust, Locust):
@@ -47,4 +48,4 @@ def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0):

task_dict[locust.__name__] = d

return task_dict
return task_dict
20 changes: 10 additions & 10 deletions locust/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import locust
import runners
from . import runners

import gevent
import sys
@@ -10,13 +10,13 @@
import socket
from optparse import OptionParser

import web
from log import setup_logging, console_logger
from stats import stats_printer, print_percentile_stats, print_error_report, print_stats
from inspectlocust import print_task_ratio, get_task_ratio_dict
from core import Locust, HttpLocust
from runners import MasterLocustRunner, SlaveLocustRunner, LocalLocustRunner
import events
from . import web
from .log import setup_logging, console_logger
from .stats import stats_printer, print_percentile_stats, print_error_report, print_stats
from .inspectlocust import print_task_ratio, get_task_ratio_dict
from .core import Locust, HttpLocust
from .runners import MasterLocustRunner, SlaveLocustRunner, LocalLocustRunner
from . import events

_internals = [Locust, HttpLocust]
version = locust.__version__
@@ -338,7 +338,7 @@ def main():
logger = logging.getLogger(__name__)

if options.show_version:
print "Locust %s" % (version,)
print("Locust %s" % (version,))
sys.exit(0)

locustfile = find_locustfile(options.locustfile)
@@ -409,7 +409,7 @@ def main():
try:
runners.locust_runner = SlaveLocustRunner(locust_classes, options)
main_greenlet = runners.locust_runner.greenlet
except socket.error, e:
except socket.error as e:
logger.error("Failed to connect to the Locust master: %s", e)
sys.exit(-1)

4 changes: 2 additions & 2 deletions locust/rpc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import warnings

try:
import zmqrpc as rpc
from . import zmqrpc as rpc
except ImportError:
warnings.warn("WARNING: Using pure Python socket RPC implementation instead of zmq. If running in distributed mode, this could cause a performance decrease. We recommend you to install the pyzmq python package when running in distributed mode.")
import socketrpc as rpc
from . import socketrpc as rpc

from .protocol import Message
2 changes: 1 addition & 1 deletion locust/rpc/protocol.py
Original file line number Diff line number Diff line change
@@ -11,5 +11,5 @@ def serialize(self):

@classmethod
def unserialize(cls, data):
msg = cls(*msgpack.loads(data))
msg = cls(*msgpack.loads(data, encoding='utf-8'))
return msg
22 changes: 12 additions & 10 deletions locust/runners.py
Original file line number Diff line number Diff line change
@@ -10,11 +10,13 @@
import gevent
from gevent import GreenletExit
from gevent.pool import Group
import six
from six.moves import xrange

import events
from stats import global_stats
from . import events
from .stats import global_stats

from rpc import rpc, Message
from .rpc import rpc, Message

logger = logging.getLogger(__name__)

@@ -102,7 +104,7 @@ def hatch():
sleep_time = 1.0 / self.hatch_rate
while True:
if not bucket:
logger.info("All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in occurence_count.iteritems()]))
logger.info("All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in six.iteritems(occurence_count)]))
events.hatch_complete.fire(user_count=self.num_clients)
return

@@ -225,7 +227,7 @@ def __init__(self, *args, **kwargs):

class SlaveNodesDict(dict):
def get_by_state(self, state):
return [c for c in self.itervalues() if c.state == state]
return [c for c in six.itervalues(self) if c.state == state]

@property
def ready(self):
@@ -260,7 +262,7 @@ def on_quitting():

@property
def user_count(self):
return sum([c.user_count for c in self.clients.itervalues()])
return sum([c.user_count for c in six.itervalues(self.clients)])

def start_hatching(self, locust_count, hatch_rate):
num_slaves = len(self.clients.ready) + len(self.clients.running)
@@ -270,7 +272,7 @@ def start_hatching(self, locust_count, hatch_rate):
return

self.num_clients = locust_count
slave_num_clients = locust_count / (num_slaves or 1)
slave_num_clients = locust_count // (num_slaves or 1)
slave_hatch_rate = float(hatch_rate) / (num_slaves or 1)
remaining = locust_count % num_slaves

@@ -281,7 +283,7 @@ def start_hatching(self, locust_count, hatch_rate):
self.exceptions = {}
events.master_start_hatching.fire()

for client in self.clients.itervalues():
for client in six.itervalues(self.clients):
data = {
"hatch_rate":slave_hatch_rate,
"num_clients":slave_num_clients,
@@ -305,7 +307,7 @@ def stop(self):
events.master_stop_hatching.fire()

def quit(self):
for client in self.clients.itervalues():
for client in six.itervalues(self.clients):
self.server.send(Message("quit", None, None))
self.greenlet.kill(block=True)

@@ -332,7 +334,7 @@ def client_listener(self):
self.clients[msg.node_id].state = STATE_RUNNING
self.clients[msg.node_id].user_count = msg.data["count"]
if len(self.clients.hatching) == 0:
count = sum(c.user_count for c in self.clients.itervalues())
count = sum(c.user_count for c in six.itervalues(self.clients))
events.hatch_complete.fire(user_count=count)
elif msg.type == "quit":
if msg.node_id in self.clients:
34 changes: 18 additions & 16 deletions locust/stats.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import time
import gevent
import hashlib
import six
from six.moves import xrange

import events
from exception import StopLocust
from log import console_logger
from . import events
from .exception import StopLocust
from .log import console_logger

STATS_NAME_WIDTH = 60

@@ -38,7 +40,7 @@ def aggregated_stats(self, name="Total", full_request_history=False):
within entries.
"""
total = StatsEntry(self, name, method=None)
for r in self.entries.itervalues():
for r in six.itervalues(self.entries):
total.extend(r, full_request_history=full_request_history)
return total

@@ -49,7 +51,7 @@ def reset_all(self):
self.start_time = time.time()
self.num_requests = 0
self.num_failures = 0
for r in self.entries.itervalues():
for r in six.itervalues(self.entries):
r.reset()

def clear_all(self):
@@ -249,7 +251,7 @@ def extend(self, other, full_request_history=False):
self.num_failures = self.num_failures + other.num_failures
self.total_response_time = self.total_response_time + other.total_response_time
self.max_response_time = max(self.max_response_time, other.max_response_time)
self.min_response_time = min(self.min_response_time, other.min_response_time) or other.min_response_time
self.min_response_time = min(self.min_response_time or 0, other.min_response_time or 0) or other.min_response_time
self.total_content_length = self.total_content_length + other.total_content_length

if full_request_history:
@@ -332,7 +334,7 @@ def get_response_time_percentile(self, percent):
num_of_request = int((self.num_requests * percent))

processed_count = 0
for response_time in sorted(self.response_times.iterkeys(), reverse=True):
for response_time in sorted(six.iterkeys(self.response_times), reverse=True):
processed_count += self.response_times[response_time]
if((self.num_requests - processed_count) <= num_of_request):
return response_time
@@ -365,7 +367,7 @@ def __init__(self, method, name, error, occurences=0):
@classmethod
def create_key(cls, method, name, error):
key = "%s.%s.%r" % (method, name, error)
return hashlib.md5(key).hexdigest()
return hashlib.md5(key.encode('utf-8')).hexdigest()

def occured(self):
self.occurences += 1
@@ -401,7 +403,7 @@ def median_from_dict(total, count):
count is a dict {response_time: count}
"""
pos = (total - 1) / 2
for k in sorted(count.iterkeys()):
for k in sorted(six.iterkeys(count)):
if pos < count[k]:
return k
pos -= count[k]
@@ -423,8 +425,8 @@ def on_request_failure(request_type, name, response_time, exception):
global_stats.get(name, request_type).log_error(exception)

def on_report_to_master(client_id, data):
data["stats"] = [global_stats.entries[key].get_stripped_report() for key in global_stats.entries.iterkeys() if not (global_stats.entries[key].num_requests == 0 and global_stats.entries[key].num_failures == 0)]
data["errors"] = dict([(k, e.to_dict()) for k, e in global_stats.errors.iteritems()])
data["stats"] = [global_stats.entries[key].get_stripped_report() for key in six.iterkeys(global_stats.entries) if not (global_stats.entries[key].num_requests == 0 and global_stats.entries[key].num_failures == 0)]
data["errors"] = dict([(k, e.to_dict()) for k, e in six.iteritems(global_stats.errors)])
global_stats.errors = {}

def on_slave_report(client_id, data):
@@ -434,9 +436,9 @@ def on_slave_report(client_id, data):
if not request_key in global_stats.entries:
global_stats.entries[request_key] = StatsEntry(global_stats, entry.name, entry.method)
global_stats.entries[request_key].extend(entry, full_request_history=True)
global_stats.last_request_timestamp = max(global_stats.last_request_timestamp, entry.last_request_timestamp)
global_stats.last_request_timestamp = max(global_stats.last_request_timestamp or 0, entry.last_request_timestamp)

for error_key, error in data["errors"].iteritems():
for error_key, error in six.iteritems(data["errors"]):
if error_key not in global_stats.errors:
global_stats.errors[error_key] = StatsError.from_dict(error)
else:
@@ -454,7 +456,7 @@ def print_stats(stats):
total_rps = 0
total_reqs = 0
total_failures = 0
for key in sorted(stats.iterkeys()):
for key in sorted(six.iterkeys(stats)):
r = stats[key]
total_rps += r.current_rps
total_reqs += r.num_requests
@@ -474,7 +476,7 @@ def print_percentile_stats(stats):
console_logger.info("Percentage of the requests completed within given times")
console_logger.info((" %-" + str(STATS_NAME_WIDTH) + "s %8s %6s %6s %6s %6s %6s %6s %6s %6s %6s") % ('Name', '# reqs', '50%', '66%', '75%', '80%', '90%', '95%', '98%', '99%', '100%'))
console_logger.info("-" * (80 + STATS_NAME_WIDTH))
for key in sorted(stats.iterkeys()):
for key in sorted(six.iterkeys(stats)):
r = stats[key]
if r.response_times:
console_logger.info(r.percentile())
@@ -491,7 +493,7 @@ def print_error_report():
console_logger.info("Error report")
console_logger.info(" %-18s %-100s" % ("# occurences", "Error"))
console_logger.info("-" * (80 + STATS_NAME_WIDTH))
for error in global_stats.errors.itervalues():
for error in six.itervalues(global_stats.errors):
console_logger.info(" %-18i %-100s" % (error.occurences, error.to_name()))
console_logger.info("-" * (80 + STATS_NAME_WIDTH))
console_logger.info("")
2 changes: 1 addition & 1 deletion locust/test/test_client.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
import gevent
from locust.clients import HttpSession
from locust.stats import global_stats
from testcases import WebserverTestCase
from .testcases import WebserverTestCase

class TestHttpSession(WebserverTestCase):
def test_get(self):
25 changes: 13 additions & 12 deletions locust/test/test_locust_class.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import unittest
import six

from locust.core import HttpLocust, Locust, TaskSet, task, events
from locust import ResponseError, InterruptTaskSet
from locust.exception import CatchResponseError, RescheduleTask, RescheduleTaskImmediately, LocustError

from testcases import LocustTestCase, WebserverTestCase
from .testcases import LocustTestCase, WebserverTestCase

class TestTaskSet(LocustTestCase):
def setUp(self):
@@ -151,7 +152,7 @@ def t2(self):

l = MySubTaskSet(self.locust)
self.assertEqual(2, len(l.tasks))
self.assertEqual([t1, MySubTaskSet.t2.__func__], l.tasks)
self.assertEqual([t1, six.get_unbound_function(MySubTaskSet.t2)], l.tasks)

def test_task_decorator_with_or_without_argument(self):
class MyTaskSet(TaskSet):
@@ -328,51 +329,51 @@ class MyLocust(HttpLocust):

my_locust = MyLocust()
t1(my_locust)
self.assertEqual(self.response.content, "This is an ultra fast response")
self.assertEqual(self.response.text, "This is an ultra fast response")

def test_client_request_headers(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("hello", locust.client.get("/request_header_test", headers={"X-Header-Test":"hello"}).content)
self.assertEqual("hello", locust.client.get("/request_header_test", headers={"X-Header-Test":"hello"}).text)

def test_client_get(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("GET", locust.client.get("/request_method").content)
self.assertEqual("GET", locust.client.get("/request_method").text)

def test_client_get_absolute_url(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("GET", locust.client.get("http://127.0.0.1:%i/request_method" % self.port).content)
self.assertEqual("GET", locust.client.get("http://127.0.0.1:%i/request_method" % self.port).text)

def test_client_post(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("POST", locust.client.post("/request_method", {"arg":"hello world"}).content)
self.assertEqual("hello world", locust.client.post("/post", {"arg":"hello world"}).content)
self.assertEqual("POST", locust.client.post("/request_method", {"arg":"hello world"}).text)
self.assertEqual("hello world", locust.client.post("/post", {"arg":"hello world"}).text)

def test_client_put(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("PUT", locust.client.put("/request_method", {"arg":"hello world"}).content)
self.assertEqual("hello world", locust.client.put("/put", {"arg":"hello world"}).content)
self.assertEqual("PUT", locust.client.put("/request_method", {"arg":"hello world"}).text)
self.assertEqual("hello world", locust.client.put("/put", {"arg":"hello world"}).text)

def test_client_delete(self):
class MyLocust(HttpLocust):
host = "http://127.0.0.1:%i" % self.port

locust = MyLocust()
self.assertEqual("DELETE", locust.client.delete("/request_method").content)
self.assertEqual("DELETE", locust.client.delete("/request_method").text)
self.assertEqual(200, locust.client.delete("/request_method").status_code)

def test_client_head(self):
@@ -395,7 +396,7 @@ class MyUnauthorizedLocust(HttpLocust):
locust = MyLocust()
unauthorized = MyUnauthorizedLocust()
authorized = MyAuthorizedLocust()
self.assertEqual("Authorized", authorized.client.get("/basic_auth").content)
self.assertEqual("Authorized", authorized.client.get("/basic_auth").text)
self.assertFalse(locust.client.get("/basic_auth"))
self.assertFalse(unauthorized.client.get("/basic_auth"))

3 changes: 2 additions & 1 deletion locust/test/test_stats.py
Original file line number Diff line number Diff line change
@@ -2,8 +2,9 @@
import time

from requests.exceptions import RequestException
from six.moves import xrange

from testcases import WebserverTestCase
from .testcases import WebserverTestCase
from locust.stats import RequestStats, StatsEntry, global_stats
from locust.core import HttpLocust, Locust, TaskSet, task
from locust.inspectlocust import get_task_ratio_dict
2 changes: 1 addition & 1 deletion locust/test/test_taskratio.py
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ class UnlikelyLocust(Locust):
weight = 1
task_set = Tasks

class MoreLikelyLocust(Locust):
class MoreLikelyLocust(Locust):
weight = 3
task_set = Tasks

12 changes: 6 additions & 6 deletions locust/test/test_web.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import json
import sys
import traceback
from StringIO import StringIO
from six.moves import StringIO

import requests
import mock
@@ -43,7 +43,7 @@ def test_stats(self):
response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port)
self.assertEqual(200, response.status_code)

data = json.loads(response.content)
data = json.loads(response.text)
self.assertEqual(2, len(data["stats"])) # one entry plus Total
self.assertEqual("/test", data["stats"][0]["name"])
self.assertEqual("GET", data["stats"][0]["method"])
@@ -53,17 +53,17 @@ def test_stats_cache(self):
stats.global_stats.get("/test", "GET").log(120, 5612)
response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port)
self.assertEqual(200, response.status_code)
data = json.loads(response.content)
data = json.loads(response.text)
self.assertEqual(2, len(data["stats"])) # one entry plus Total

# add another entry
stats.global_stats.get("/test2", "GET").log(120, 5612)
data = json.loads(requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).content)
data = json.loads(requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).text)
self.assertEqual(2, len(data["stats"])) # old value should be cached now

web.request_stats.clear_cache()

data = json.loads(requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).content)
data = json.loads(requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).text)
self.assertEqual(3, len(data["stats"])) # this should no longer be cached

def test_request_stats_csv(self):
@@ -87,7 +87,7 @@ def test_exceptions_csv(self):
response = requests.get("http://127.0.0.1:%i/exceptions/csv" % self.web_port)
self.assertEqual(200, response.status_code)

reader = csv.reader(StringIO(response.content))
reader = csv.reader(StringIO(response.text))
rows = []
for row in reader:
rows.append(row)
13 changes: 9 additions & 4 deletions locust/test/testcases.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,9 @@
import random
import unittest
from copy import copy
from StringIO import StringIO
from io import BytesIO
import sys
import six

from locust import events
from locust.stats import global_stats
@@ -79,7 +81,7 @@ def do_redirect():

@app.route("/basic_auth")
def basic_auth():
auth = base64.b64decode(request.headers.get("Authorization").replace("Basic ", ""))
auth = base64.b64decode(request.headers.get("Authorization").replace("Basic ", "")).decode('utf-8')
if auth == "locust:menace":
return "Authorized"
resp = make_response("401 Authorization Required", 401)
@@ -88,7 +90,7 @@ def basic_auth():

@app.route("/no_content_length")
def no_content_length():
r = send_file(StringIO("This response does not have content-length in the header"), add_etags=False)
r = send_file(BytesIO("This response does not have content-length in the header".encode('utf-8')), add_etags=False)
return r

@app.errorhandler(404)
@@ -113,14 +115,17 @@ class LocustTestCase(unittest.TestCase):
safe to register any custom event handlers within the test.
"""
def setUp(self):
# Prevent args passed to test runner from being passed to Locust
del sys.argv[1:]

self._event_handlers = {}
for name in dir(events):
event = getattr(events, name)
if isinstance(event, events.EventHook):
self._event_handlers[event] = copy(event._handlers)

def tearDown(self):
for event, handlers in self._event_handlers.iteritems():
for event, handlers in six.iteritems(self._event_handlers):
event._handlers = handlers

def assertIn(self, member, container, msg=None):
11 changes: 6 additions & 5 deletions locust/web.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@
from time import time
from itertools import chain
from collections import defaultdict
from StringIO import StringIO
from six.moves import StringIO, xrange
import six

from gevent import wsgi
from flask import Flask, make_response, request, render_template
@@ -150,7 +151,7 @@ def request_stats():
"avg_content_length": s.avg_content_length,
})

report = {"stats":stats, "errors":[e.to_dict() for e in runners.locust_runner.errors.itervalues()]}
report = {"stats":stats, "errors":[e.to_dict() for e in six.itervalues(runners.locust_runner.errors)]}
if stats:
report["total_rps"] = stats[len(stats)-1]["current_rps"]
report["fail_ratio"] = runners.locust_runner.stats.aggregated_stats("Total").fail_ratio
@@ -176,7 +177,7 @@ def request_stats():

@app.route("/exceptions")
def exceptions():
response = make_response(json.dumps({'exceptions': [{"count": row["count"], "msg": row["msg"], "traceback": row["traceback"], "nodes" : ", ".join(row["nodes"])} for row in runners.locust_runner.exceptions.itervalues()]}))
response = make_response(json.dumps({'exceptions': [{"count": row["count"], "msg": row["msg"], "traceback": row["traceback"], "nodes" : ", ".join(row["nodes"])} for row in six.itervalues(runners.locust_runner.exceptions)]}))
response.headers["Content-type"] = "application/json"
return response

@@ -185,7 +186,7 @@ def exceptions_csv():
data = StringIO()
writer = csv.writer(data)
writer.writerow(["Count", "Message", "Traceback", "Nodes"])
for exc in runners.locust_runner.exceptions.itervalues():
for exc in six.itervalues(runners.locust_runner.exceptions):
nodes = ", ".join(exc["nodes"])
writer.writerow([exc["count"], exc["msg"], exc["traceback"], nodes])

@@ -201,4 +202,4 @@ def start(locust, options):
wsgi.WSGIServer((options.web_host, options.port), app, log=None).serve_forever()

def _sort_stats(stats):
return [stats[key] for key in sorted(stats.iterkeys())]
return [stats[key] for key in sorted(six.iterkeys(stats))]
26 changes: 6 additions & 20 deletions setup.py
Original file line number Diff line number Diff line change
@@ -11,23 +11,6 @@
version = str(ast.literal_eval(_version_re.search(
f.read().decode('utf-8')).group(1)))


class Unit2Discover(Command):
user_options = []

def initialize_options(self):
pass

def finalize_options(self):
pass

def run(self):
import sys, subprocess
basecmd = ['unit2', 'discover']
errno = subprocess.call(basecmd)
raise SystemExit(errno)


setup(
name='locustio',
version=version,
@@ -42,6 +25,10 @@ def run(self):
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
],
@@ -53,12 +40,11 @@ def run(self):
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True,
zip_safe=False,
install_requires=["gevent==1.1.1", "flask>=0.10.1", "requests>=2.9.1", "msgpack-python>=0.4.2"],
tests_require=['unittest2', 'mock', 'pyzmq'],
install_requires=["gevent==1.1.1", "flask>=0.10.1", "requests>=2.9.1", "msgpack-python>=0.4.2", "six>=1.10.0", "pyzmq==15.2.0"],
tests_require=['unittest2', 'mock'],
entry_points={
'console_scripts': [
'locust = locust.main:main',
]
},
test_suite='unittest2.collector',
)
10 changes: 10 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[tox]
envlist = py26, py27, py33, py34, py35

[testenv]
deps =
mock
pyzmq
unittest2
commands =
unit2 discover []

0 comments on commit 9e202a3

Please sign in to comment.