diff --git a/.gitignore b/.gitignore index c8d25079a1..faea13ed42 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist/** .vagrant build/ .coverage +.tox/ diff --git a/.travis.yml b/.travis.yml index f9ca87101e..9e9fedd396 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Makefile b/Makefile index f5260bdd24..a88395564e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - python setup.py test + unit2 discover release: python setup.py sdist upload diff --git a/README.md b/README.md index 5c2d0e124c..4b637966a0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/locust/__init__.py b/locust/__init__.py index 4ee77fdfaa..70c08e7476 100644 --- a/locust/__init__.py +++ b/locust/__init__.py @@ -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" diff --git a/locust/clients.py b/locust/clients.py index 307d267cb1..508c4cb47c 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -1,7 +1,8 @@ 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 @@ -9,8 +10,8 @@ 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( diff --git a/locust/core.py b/locust/core.py index bdfd7b150b..5328f4a291 100644 --- a/locust/core.py +++ b/locust/core.py @@ -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): diff --git a/locust/inspectlocust.py b/locust/inspectlocust.py index bcfa5a7598..15555341e8 100644 --- a/locust/inspectlocust.py +++ b/locust/inspectlocust.py @@ -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 \ No newline at end of file + return task_dict diff --git a/locust/main.py b/locust/main.py index 249646e3f3..ba57010361 100644 --- a/locust/main.py +++ b/locust/main.py @@ -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) diff --git a/locust/rpc/__init__.py b/locust/rpc/__init__.py index 65e753e8d3..fa0e067208 100644 --- a/locust/rpc/__init__.py +++ b/locust/rpc/__init__.py @@ -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 diff --git a/locust/rpc/protocol.py b/locust/rpc/protocol.py index e2b8fca347..66ce2c72cf 100644 --- a/locust/rpc/protocol.py +++ b/locust/rpc/protocol.py @@ -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 diff --git a/locust/runners.py b/locust/runners.py index 3ec811e384..970d2b03b1 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -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: diff --git a/locust/stats.py b/locust/stats.py index 267dee4e4e..178c982b82 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -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("") diff --git a/locust/test/test_client.py b/locust/test/test_client.py index 6bbbc7a04b..712e3c1fb3 100644 --- a/locust/test/test_client.py +++ b/locust/test/test_client.py @@ -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): diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index efef92da62..48e162ebfa 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -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")) diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index 550c0abaf8..722e758a28 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -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 diff --git a/locust/test/test_taskratio.py b/locust/test/test_taskratio.py index 12e68500c4..941db73e18 100644 --- a/locust/test/test_taskratio.py +++ b/locust/test/test_taskratio.py @@ -50,7 +50,7 @@ class UnlikelyLocust(Locust): weight = 1 task_set = Tasks - class MoreLikelyLocust(Locust): + class MoreLikelyLocust(Locust): weight = 3 task_set = Tasks diff --git a/locust/test/test_web.py b/locust/test/test_web.py index dad15fc412..5a7004f933 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -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) diff --git a/locust/test/testcases.py b/locust/test/testcases.py index 274c42cb4d..dfe201f9cc 100644 --- a/locust/test/testcases.py +++ b/locust/test/testcases.py @@ -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,6 +115,9 @@ 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) @@ -120,7 +125,7 @@ def setUp(self): 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): diff --git a/locust/web.py b/locust/web.py index 9235f6962e..ce9f3a63b7 100644 --- a/locust/web.py +++ b/locust/web.py @@ -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))] diff --git a/setup.py b/setup.py index 6ff36085f1..9078b14ada 100644 --- a/setup.py +++ b/setup.py @@ -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', ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..ad926ac58e --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py26, py27, py33, py34, py35 + +[testenv] +deps = + mock + pyzmq + unittest2 +commands = + unit2 discover []