` attribute of the root
Locust instance.
- """
+ """
return self.locust.client
@property
@@ -365,6 +370,13 @@ def configuration(self):
Reference to configuration.py
"""
return self.config.read_json()
+
+ def team_configuration(self, path):
+ """
+ Reference to get_team_config func in configuration.py
+ """
+ config = ClientConfiguration()
+ return config.read_json(path)
@property
def runners(self):
diff --git a/locust/inspectlocust.py b/locust/inspectlocust.py
index 15555341e8..51ab1e9184 100644
--- a/locust/inspectlocust.py
+++ b/locust/inspectlocust.py
@@ -1,9 +1,11 @@
import inspect
+
import six
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)
diff --git a/locust/log.py b/locust/log.py
index 24ffffbef6..5c824f5c18 100644
--- a/locust/log.py
+++ b/locust/log.py
@@ -1,6 +1,6 @@
import logging
-import sys
import socket
+import sys
host = socket.gethostname()
diff --git a/locust/main.py b/locust/main.py
index 98c2599f99..f6dc1bcb1c 100644
--- a/locust/main.py
+++ b/locust/main.py
@@ -1,26 +1,25 @@
-import locust
-from . import runners
-
-import imp
-import gevent
-import sys
-import os
-import signal
import inspect
import logging
+import os
+import signal
import socket
+import sys
import time
import tests_loader
import fileio
from optparse import OptionParser
-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
+import gevent
+
+import locust
+
+from . import events, runners, web
+from .core import HttpLocust, Locust
+from .inspectlocust import get_task_ratio_dict, print_task_ratio
+from .log import console_logger, setup_logging
+from .runners import LocalLocustRunner, MasterLocustRunner, SlaveLocustRunner
+from .stats import (print_error_report, print_percentile_stats, print_stats,
+ stats_printer, stats_writer, write_stat_csvs)
_internals = [Locust, HttpLocust]
version = locust.__version__
@@ -48,7 +47,7 @@ def parse_options():
default="",
help="Host to bind the web interface to. Defaults to '' (all interfaces)"
)
-
+
parser.add_option(
'-P', '--port', '--web-port',
type="int",
@@ -56,7 +55,7 @@ def parse_options():
default=8089,
help="Port on which to run web host"
)
-
+
parser.add_option(
'-f', '--locustfile',
dest='locustfile',
@@ -64,6 +63,16 @@ def parse_options():
help="Python module file to import, e.g. '../other.py'. Default: locustfile"
)
+ # A file that contains the current request stats.
+ parser.add_option(
+ '--csv', '--csv-base-name',
+ action='store',
+ type='str',
+ dest='csvfilebase',
+ default=None,
+ help="Store current request stats to files in CSV format.",
+ )
+
# if locust should be run in distributed mode as master
parser.add_option(
'--master',
@@ -81,7 +90,7 @@ def parse_options():
default=False,
help="Set locust to run in distributed mode with this process as slave"
)
-
+
# master host options
parser.add_option(
'--master-host',
@@ -91,7 +100,7 @@ def parse_options():
default="127.0.0.1",
help="Host or IP address of locust master for distributed load testing. Only used when running with --slave. Defaults to 127.0.0.1."
)
-
+
parser.add_option(
'--master-port',
action='store',
@@ -109,7 +118,7 @@ def parse_options():
default="*",
help="Interfaces (hostname, ip) that locust master should bind to. Only used when running with --master. Defaults to * (all available interfaces)."
)
-
+
parser.add_option(
'--master-bind-port',
action='store',
@@ -156,17 +165,18 @@ def parse_options():
default=1,
help="The rate per second in which clients are spawned. Only used together with --no-web"
)
-
+
# Number of requests
parser.add_option(
'-n', '--num-request',
action='store',
+
type='int',
dest='num_requests',
default=None,
help="Number of requests to perform. Only used together with --no-web"
)
-
+
# log level
parser.add_option(
'--loglevel', '-L',
@@ -176,7 +186,7 @@ def parse_options():
default='INFO',
help="Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO.",
)
-
+
# log file
parser.add_option(
'--logfile',
@@ -186,7 +196,7 @@ def parse_options():
default=None,
help="Path to log file. If not set, log will go to stdout/stderr",
)
-
+
# if we should print stats in the console
parser.add_option(
'--print-stats',
@@ -206,13 +216,13 @@ def parse_options():
)
parser.add_option(
- '--no-reset-stats',
+ '--reset-stats',
action='store_true',
- dest='no_reset_stats',
+ dest='reset_stats',
default=False,
- help="Do not reset statistics once hatching has been completed",
+ help="Reset statistics once hatching has been completed",
)
-
+
# List locust commands found in loaded locust files/source files
parser.add_option(
'-l', '--list',
@@ -221,7 +231,7 @@ def parse_options():
default=False,
help="Show list of possible locust classes and exit"
)
-
+
# Display ratio table of all tasks
parser.add_option(
'--show-task-ratio',
@@ -238,7 +248,7 @@ def parse_options():
default=False,
help="print json data of the locust classes' task execution ratio"
)
-
+
# Version number (optparse gives you --version but we have to do it
# ourselves to get -V too. sigh)
parser.add_option(
@@ -315,7 +325,7 @@ def main():
else:
# list() call is needed to consume the dict_view object in Python 3
locust_classes = list(locusts.values())
-
+
if options.show_task_ratio:
console_logger.info("\n Task ratio per locust class")
console_logger.info( "-" * 80)
@@ -337,7 +347,7 @@ def main():
# spawn web greenlet
logger.info("Starting web monitor at %s:%s" % (options.web_host or "*", options.port))
main_greenlet = gevent.spawn(web.start, locust_classes, options)
-
+
if not options.master and not options.slave:
runners.locust_runner = LocalLocustRunner(locust_classes, options, available_locustfiles=all_locustfiles)
# spawn client spawning/hatching greenlet
@@ -361,30 +371,35 @@ def main():
except socket.error as e:
logger.error("Failed to connect to the Locust master: %s", e)
sys.exit(-1)
-
+
if not options.only_summary and (options.print_stats or (options.no_web and not options.slave)):
# spawn stats printing greenlet
gevent.spawn(stats_printer)
+ if options.csvfilebase:
+ gevent.spawn(stats_writer, options.csvfilebase)
+
+
def shutdown(code=0):
"""
- Shut down locust by firing quitting event, printing stats and exiting
+ Shut down locust by firing quitting event, printing/writing stats and exiting
"""
logger.info("Shutting down (exit code %s), bye." % code)
events.quitting.fire()
print_stats(runners.locust_runner.request_stats)
print_percentile_stats(runners.locust_runner.request_stats)
-
+ if options.csvfilebase:
+ write_stat_csvs(options.csvfilebase)
print_error_report()
sys.exit(code)
-
+
# install SIGTERM handler
def sig_term_handler():
logger.info("Got SIGTERM signal")
shutdown(0)
gevent.signal(signal.SIGTERM, sig_term_handler)
-
+
try:
logger.info("Starting Locust %s" % version)
main_greenlet.join()
diff --git a/locust/rpc/protocol.py b/locust/rpc/protocol.py
index 66ce2c72cf..b30a36f8bc 100644
--- a/locust/rpc/protocol.py
+++ b/locust/rpc/protocol.py
@@ -1,5 +1,6 @@
import msgpack
+
class Message(object):
def __init__(self, message_type, data, node_id):
self.type = message_type
diff --git a/locust/rpc/socketrpc.py b/locust/rpc/socketrpc.py
index 034efe6ff7..89c5b05ae1 100644
--- a/locust/rpc/socketrpc.py
+++ b/locust/rpc/socketrpc.py
@@ -1,10 +1,11 @@
-import struct
import logging
+import struct
import gevent
from gevent import socket
from locust.exception import LocustError
+
from .protocol import Message
logger = logging.getLogger(__name__)
diff --git a/locust/rpc/zmqrpc.py b/locust/rpc/zmqrpc.py
index 44a3bff13b..07583cb0a7 100644
--- a/locust/rpc/zmqrpc.py
+++ b/locust/rpc/zmqrpc.py
@@ -1,6 +1,8 @@
import zmq.green as zmq
+
from .protocol import Message
+
class BaseSocket(object):
def send(self, msg):
diff --git a/locust/runners.py b/locust/runners.py
index 0bb779d639..a43f4c1786 100644
--- a/locust/runners.py
+++ b/locust/runners.py
@@ -1,23 +1,22 @@
-# coding=UTF-8
+# -*- coding: utf-8 -*-
+import logging
+import random
import socket
import traceback
import warnings
-import random
-import logging
-from time import time
from hashlib import md5
+from time import time
import gevent
+import six
from gevent import GreenletExit
from gevent.pool import Group
-import six
+
from six.moves import xrange
from . import events, configuration
+from .rpc import Message, rpc
from .stats import global_stats
-
-from .rpc import rpc, Message
-
import fileio
import tests_loader
@@ -50,19 +49,19 @@ def __init__(self, locust_classes, options, available_locustfiles=None):
# register listener that resets stats when hatching is complete
def on_hatch_complete(user_count):
self.state = STATE_RUNNING
- if not self.options.no_reset_stats:
- logger.info("Resetting stats is skipped\n")
- # self.stats.reset_all()
+ if self.options.reset_stats:
+ logger.info("Resetting stats\n")
+ self.stats.reset_all()
events.hatch_complete += on_hatch_complete
@property
def request_stats(self):
return self.stats.entries
-
+
@property
def errors(self):
return self.stats.errors
-
+
@property
def user_count(self):
return len(self.locusts)
@@ -107,7 +106,7 @@ def spawn_locusts(self, spawn_count=None, stop_timeout=None, wait=False):
logger.info("Hatching and swarming %i clients at the rate %g clients/s..." % (spawn_count, self.hatch_rate))
occurence_count = dict([(l.__name__, 0) for l in self.locust_classes])
-
+
def hatch():
sleep_time = 1.0 / self.hatch_rate
while True:
@@ -127,7 +126,7 @@ def start_locust(_):
if len(self.locusts) % 10 == 0:
logger.debug("%i locusts hatched" % len(self.locusts))
gevent.sleep(sleep_time)
-
+
hatch()
if wait:
self.locusts.join()
@@ -231,7 +230,7 @@ def __init__(self, locust_classes, options, available_locustfiles=None):
self.master_port = options.master_port
self.master_bind_host = options.master_bind_host
self.master_bind_port = options.master_bind_port
-
+
def noop(self, *args, **kwargs):
""" Used to link() greenlets to in order to be compatible with gevent 1.0 """
pass
@@ -245,27 +244,28 @@ def __init__(self, id, state=STATE_INIT):
class MasterLocustRunner(DistributedLocustRunner):
def __init__(self, *args, **kwargs):
super(MasterLocustRunner, self).__init__(*args, **kwargs)
-
+
class SlaveNodesDict(dict):
def get_by_state(self, state):
return [c for c in six.itervalues(self) if c.state == state]
-
+
@property
def ready(self):
return self.get_by_state(STATE_INIT)
-
+
@property
def hatching(self):
return self.get_by_state(STATE_HATCHING)
-
+
@property
def running(self):
return self.get_by_state(STATE_RUNNING)
+
self.clients = SlaveNodesDict()
self.server = rpc.Server(self.master_bind_host, self.master_bind_port)
self.greenlet = Group()
self.greenlet.spawn(self.client_listener).link_exception(callback=self.noop)
-
+
# listener that gathers info on how many locust users the slaves has spawned
def on_slave_report(client_id, data):
if client_id not in self.clients:
@@ -274,12 +274,12 @@ def on_slave_report(client_id, data):
self.clients[client_id].user_count = data["user_count"]
events.slave_report += on_slave_report
-
+
# register listener that sends quit message to slave nodes
def on_quitting():
self.quit()
events.quitting += on_quitting
-
+
def on_locust_switch_file(locust_classes_key):
for client in six.itervalues(self.clients):
self.server.send(Message("switch", locust_classes_key, None))
@@ -304,7 +304,7 @@ def on_master_new_file_uploaded(new_file):
@property
def user_count(self):
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)
if not num_slaves:
@@ -340,7 +340,7 @@ def start_hatching(self, locust_count, hatch_rate):
remaining -= 1
self.server.send(Message("hatch", data, None))
-
+
self.stats.start_time = time()
self.state = STATE_HATCHING
@@ -348,12 +348,12 @@ def stop(self):
for client in self.clients.hatching + self.clients.running:
self.server.send(Message("stop", None, None))
events.master_stop_hatching.fire()
-
+
def quit(self):
for client in six.itervalues(self.clients):
self.server.send(Message("quit", None, None))
self.greenlet.kill(block=True)
-
+
def client_listener(self):
while True:
msg = self.server.recv()
@@ -394,24 +394,24 @@ class SlaveLocustRunner(DistributedLocustRunner):
def __init__(self, *args, **kwargs):
super(SlaveLocustRunner, self).__init__(*args, **kwargs)
self.client_id = socket.gethostname() + "_" + md5(str(time() + random.randint(0,10000)).encode('utf-8')).hexdigest()
-
+
self.client = rpc.Client(self.master_host, self.master_port)
self.greenlet = Group()
self.greenlet.spawn(self.worker).link_exception(callback=self.noop)
self.client.send(Message("client_ready", None, self.client_id))
self.greenlet.spawn(self.stats_reporter).link_exception(callback=self.noop)
-
+
# register listener for when all locust users have hatched, and report it to the master node
def on_hatch_complete(user_count):
self.client.send(Message("hatch_complete", {"count":user_count}, self.client_id))
events.hatch_complete += on_hatch_complete
-
- # register listener that adds the current number of spawned locusts to the report that is sent to the master node
+
+ # register listener that adds the current number of spawned locusts to the report that is sent to the master node
def on_report_to_master(client_id, data):
data["user_count"] = self.user_count
events.report_to_master += on_report_to_master
-
+
# register listener that sends quit message to master
def on_quitting():
self.client.send(Message("quit", None, self.client_id))
@@ -468,5 +468,5 @@ def stats_reporter(self):
except:
logger.error("Connection lost to master server. Aborting...")
break
-
+
gevent.sleep(SLAVE_REPORT_INTERVAL)
diff --git a/locust/static/locust.js b/locust/static/locust.js
index 28e3b976e0..c9ec284c67 100644
--- a/locust/static/locust.js
+++ b/locust/static/locust.js
@@ -170,19 +170,27 @@ $(".edit_config_link").click(function(event) {
});
-$('#upload_btn_submit').click(function(event){
+$("#directories .select2").select2({
+ placeholder: "Select a state"
+});
+
+let whichform = $('.upload_file_form_test_file')[0];
+
+$('#upload_py_submit').click(function(event){
event.preventDefault();
- $('#upload_file_form').submit();
+ whichform = $('.upload_file_form_test_file')[0];
+ $('.upload_file_form_test_file').submit();
});
-$("#directories .select2").select2({
- placeholder: "Select a state"
+$('#upload_json_submit').click(function(event){
+ event.preventDefault();
+ whichform = $('.upload_file_form_json')[0];
+ $('.upload_file_form_json').submit();
});
-$('#upload_file_form').submit(function(event) {
+$('.upload_file_form_test_file, .upload_file_form_json').submit(function(event) {
event.preventDefault();
- var form = $('#upload_file_form')[0];
- var form_data = new FormData(form);
+ var form_data = new FormData(whichform);
$.ajax({
type: 'POST',
url: "/upload_file",
@@ -201,7 +209,6 @@ $('#upload_file_form').submit(function(event) {
})
});
-
$('#submit_json_btn').click(function(){
event.preventDefault();
$('#hidden_config_json').val(JSON.stringify(json_editor.get(), null , 4));
diff --git a/locust/static/style.css b/locust/static/style.css
index 20480eed75..92994f4096 100644
--- a/locust/static/style.css
+++ b/locust/static/style.css
@@ -308,7 +308,7 @@ label[for="json_option"]+div{
.start button,
.edit button, .edit_config button[type=submit], .multiple_column button,
-#new-test-confirmation .btn-newtest, #upload_btn_submit {
+#new-test-confirmation .btn-newtest, #upload_json_submit, #upload_py_submit {
margin: 20px 0px 20px 0px;
float: right;
font-size: 14px;
@@ -566,7 +566,7 @@ ul.tabs li a.current:after {
}
-#add-new-file-modal, #new-test-confirmation {
+#add-new-file-modal, #upload-json-modal, #new-test-confirmation {
width: 398px;
position: absolute;
left: 50%;
@@ -580,33 +580,36 @@ ul.tabs li a.current:after {
background: #132b21;
box-shadow: 0 0 120px rgba(0,0,0,0.3);
}
-#add-new-file-modal{
+
+#add-new-file-modal, #upload-json-modal {
padding-left : 1%;
padding-right: 1%;
height: 275px;
}
#new-test-confirmation {
height: 160px;
+ width: 450px;
}
-#add-new-file-modal .modal-dialog, #new-test-confirmation .modal-dialog{
+
+#add-new-file-modal .modal-dialog, #new-test-confirmation .modal-dialog, #upload-json-modal .modal-dialog{
display: flex;
flex-direction: column;
vertical-align: middle;
margin-top: 0px;
margin-bottom: -50px;
}
-#add-new-file-modal .modal-header, #new-test-confirmation .modal-header {
+#add-new-file-modal .modal-header, #new-test-confirmation .modal-header, #upload-json-modal .modal-header {
border-bottom-color: transparent;
}
-#add-new-file-modal .close, #new-test-confirmation .close {
+#add-new-file-modal .close, #new-test-confirmation .close, #upload-json-modal .close {
color: #addf82;
}
-#new-test-confirmation .modal-header .close, #add-new-file-modal .close{
+#new-test-confirmation .modal-header .close, #add-new-file-modal .close, #upload-json-modal .modal-header .close{
margin-right: -25px;
padding: 3px;
}
-#new-test-confirmation .modal-body, #add-new-file-modal .modal-body {
+#new-test-confirmation .modal-body, #add-new-file-modal .modal-body, #upload-json-modal .modal-body {
padding-top: 25px;
padding-bottom: 0px;
color: white;
@@ -616,17 +619,20 @@ ul.tabs li a.current:after {
padding: 0;
}
#new-test-confirmation .btn-newtest {
- padding: 8px 30px;
- margin: 3px 3px;
+ padding: 10px 30px;
+ margin: 4px 4px;
+}
+#new-test-confirmation .modal-header .close {
+ margin-right: -10px;
}
-#add-new-file-modal .modal-content, #new-test-confirmation .modal-content {
+#add-new-file-modal .modal-content, #new-test-confirmation .modal-content, #upload-json-modal .modal-content {
background-color: #132b21;
border-color: transparent;
}
-#add-new-file-modal .modal-title, #new-test-confirmation .modal-title {
+#add-new-file-modal .modal-title, #new-test-confirmation .modal-title, #upload-json-modal .modal-title {
color:#addf82;
}
-#upload_file_form, #new-test-confirmation {
+.upload_file_form_test_file, .upload_file_form_json, #new-test-confirmation {
color: white;
}
.about {
diff --git a/locust/stats.py b/locust/stats.py
index 7091bac5b2..ea1f76ffb7 100644
--- a/locust/stats.py
+++ b/locust/stats.py
@@ -1,7 +1,10 @@
+import hashlib
import time
+from itertools import chain
+
import gevent
-import hashlib
import six
+
from six.moves import xrange
from . import events
@@ -10,6 +13,13 @@
STATS_NAME_WIDTH = 60
+"""Default interval for how frequently the CSV file is written if this option
+is configured."""
+CSV_STATS_INTERVAL_SEC = 2
+
+"""Default interval for how frequently results are written to console."""
+CONSOLE_STATS_INTERVAL_SEC = 2
+
class RequestStatsAdditionError(Exception):
pass
@@ -532,7 +542,86 @@ def print_error_report():
console_logger.info("")
def stats_printer():
- from .runners import locust_runner
+ from . import runners
+ while True:
+ print_stats(runners.locust_runner.request_stats)
+ gevent.sleep(CONSOLE_STATS_INTERVAL_SEC)
+
+def stats_writer(base_filepath):
+ """Writes the csv files for the locust run."""
while True:
- print_stats(locust_runner.request_stats)
- gevent.sleep(2)
+ write_stat_csvs(base_filepath)
+ gevent.sleep(CSV_STATS_INTERVAL_SEC)
+
+
+def write_stat_csvs(base_filepath):
+ """Writes the requests and distribution csvs."""
+ with open(base_filepath + '_requests.csv', "w") as f:
+ f.write(requests_csv())
+
+ with open(base_filepath + '_distribution.csv', 'w') as f:
+ f.write(distribution_csv())
+
+
+def sort_stats(stats):
+ return [stats[key] for key in sorted(six.iterkeys(stats))]
+
+
+def requests_csv():
+ from . import runners
+
+ """Returns the contents of the 'requests' tab as CSV."""
+ rows = [
+ ",".join([
+ '"Method"',
+ '"Name"',
+ '"# requests"',
+ '"# failures"',
+ '"Median response time"',
+ '"Average response time"',
+ '"Min response time"',
+ '"Max response time"',
+ '"Average Content Size"',
+ '"Requests/s"',
+ ])
+ ]
+
+ for s in chain(sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total", full_request_history=True)]):
+ rows.append('"%s","%s",%i,%i,%i,%i,%i,%i,%i,%.2f' % (
+ s.method,
+ s.name,
+ s.num_requests,
+ s.num_failures,
+ s.median_response_time,
+ s.avg_response_time,
+ s.min_response_time or 0,
+ s.max_response_time,
+ s.avg_content_length,
+ s.total_rps,
+ ))
+ return "\n".join(rows)
+
+def distribution_csv():
+ """Returns the contents of the 'distribution' tab as CSV."""
+ from . import runners
+
+ rows = [",".join((
+ '"Name"',
+ '"# requests"',
+ '"50%"',
+ '"66%"',
+ '"75%"',
+ '"80%"',
+ '"90%"',
+ '"95%"',
+ '"98%"',
+ '"99%"',
+ '"100%"',
+ ))]
+ for s in chain(sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total", full_request_history=True)]):
+ if s.num_requests:
+ rows.append(s.percentile(tpl='"%s",%i,%i,%i,%i,%i,%i,%i,%i,%i,%i'))
+ else:
+ rows.append('"%s",0,"N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A"' % s.name)
+
+ return "\n".join(rows)
diff --git a/locust/templates/index.html b/locust/templates/index.html
index 2a7fbf74d1..96194d03d0 100644
--- a/locust/templates/index.html
+++ b/locust/templates/index.html
@@ -132,7 +132,7 @@ Are you sure?
Upload Test File
-
@@ -230,7 +230,7 @@ Ramping
-
+
- New Test
{% if ramp %}
@@ -239,6 +239,7 @@ Ramping
+
+
+
+
+
+
+
+
+
diff --git a/locust/test/test_client.py b/locust/test/test_client.py
index d76630bcf4..197274aa74 100644
--- a/locust/test/test_client.py
+++ b/locust/test/test_client.py
@@ -1,10 +1,12 @@
-from requests.exceptions import (RequestException, MissingSchema,
- InvalidSchema, InvalidURL)
+from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
+ RequestException)
from locust.clients import HttpSession
from locust.stats import global_stats
+
from .testcases import WebserverTestCase
+
class TestHttpSession(WebserverTestCase):
def test_get(self):
s = HttpSession("http://127.0.0.1:%i" % self.port)
@@ -66,4 +68,3 @@ def test_post_redirect(self):
get_stats = global_stats.get(url, method="GET")
self.assertEqual(1, post_stats.num_requests)
self.assertEqual(0, get_stats.num_requests)
-
diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py
index f2da60a275..e1198f3f93 100644
--- a/locust/test/test_locust_class.py
+++ b/locust/test/test_locust_class.py
@@ -1,11 +1,13 @@
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 locust import InterruptTaskSet, ResponseError
+from locust.core import HttpLocust, Locust, TaskSet, events, task
+from locust.exception import (CatchResponseError, LocustError, RescheduleTask,
+ RescheduleTaskImmediately)
from .testcases import LocustTestCase, WebserverTestCase
+
class TestTaskSet(LocustTestCase):
def setUp(self):
super(TestTaskSet, self).setUp()
diff --git a/locust/test/test_main.py b/locust/test/test_main.py
index 902ecde8ff..6686c5d281 100644
--- a/locust/test/test_main.py
+++ b/locust/test/test_main.py
@@ -1,7 +1,9 @@
-from locust.core import HttpLocust, Locust, TaskSet
from locust import main
+from locust.core import HttpLocust, Locust, TaskSet
+
from .testcases import LocustTestCase
+
class TestTaskSet(LocustTestCase):
def test_is_locust(self):
self.assertFalse(main.is_locust(("Locust", Locust)))
diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py
index 31bbeae697..b785410222 100644
--- a/locust/test/test_runners.py
+++ b/locust/test/test_runners.py
@@ -1,19 +1,18 @@
import unittest
import gevent
-import mock
-
-from gevent.queue import Queue
from gevent import sleep
+from gevent.queue import Queue
-from locust.runners import LocalLocustRunner, MasterLocustRunner
-from locust.core import Locust, task, TaskSet
+import mock
+from locust import events
+from locust.core import Locust, TaskSet, task
from locust.exception import LocustError
+from locust.main import parse_options
from locust.rpc import Message
+from locust.runners import LocalLocustRunner, MasterLocustRunner
from locust.stats import global_stats
-from locust.main import parse_options
from locust.test.testcases import LocustTestCase
-from locust import events
def mocked_rpc_server():
@@ -246,4 +245,3 @@ def test_message_serialize(self):
self.assertEqual(msg.type, rebuilt.type)
self.assertEqual(msg.data, rebuilt.data)
self.assertEqual(msg.node_id, rebuilt.node_id)
-
diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py
index 41784f2fed..6d4d646f97 100644
--- a/locust/test/test_stats.py
+++ b/locust/test/test_stats.py
@@ -1,13 +1,14 @@
-import unittest
import time
+import unittest
-from six.moves import xrange
-
-from .testcases import WebserverTestCase
-from locust.stats import RequestStats, StatsEntry, global_stats
from locust.core import HttpLocust, TaskSet, task
from locust.inspectlocust import get_task_ratio_dict
from locust.rpc.protocol import Message
+from locust.stats import RequestStats, StatsEntry, global_stats
+from six.moves import xrange
+
+from .testcases import WebserverTestCase
+
class TestRequestStats(unittest.TestCase):
def setUp(self):
diff --git a/locust/test/test_taskratio.py b/locust/test/test_taskratio.py
index 5081fbf5f8..f5fbecdb37 100644
--- a/locust/test/test_taskratio.py
+++ b/locust/test/test_taskratio.py
@@ -3,6 +3,7 @@
from locust.core import Locust, TaskSet, task
from locust.inspectlocust import get_task_ratio_dict
+
class TestTaskRatio(unittest.TestCase):
def test_task_ratio_command(self):
class Tasks(TaskSet):
@@ -56,7 +57,7 @@ class MoreLikelyLocust(Locust):
ratio_dict = get_task_ratio_dict([UnlikelyLocust, MoreLikelyLocust], total=True)
- self.assertEquals({
+ self.assertEqual({
'UnlikelyLocust': {'tasks': {'task1': {'ratio': 0.25*0.25}, 'task3': {'ratio': 0.25*0.75}}, 'ratio': 0.25},
'MoreLikelyLocust': {'tasks': {'task1': {'ratio': 0.75*0.25}, 'task3': {'ratio': 0.75*0.75}}, 'ratio': 0.75}
}, ratio_dict)
diff --git a/locust/test/test_web.py b/locust/test/test_web.py
index 16d95270ea..a070fc4e35 100644
--- a/locust/test/test_web.py
+++ b/locust/test/test_web.py
@@ -1,20 +1,21 @@
-# encoding: utf-8
-
+# -*- coding: utf-8 -*-
import csv
import json
import sys
import traceback
-from six.moves import StringIO
-import requests
import gevent
+import requests
from gevent import wsgi
-from locust import web, runners, stats
-from locust.runners import LocustRunner
+from locust import runners, stats, web
from locust.main import parse_options
+from locust.runners import LocustRunner
+from six.moves import StringIO
+
from .testcases import LocustTestCase
+
class TestWebUI(LocustTestCase):
def setUp(self):
super(TestWebUI, self).setUp()
@@ -119,4 +120,3 @@ def test_exceptions_csv(self):
self.assertEqual(2, len(rows))
self.assertEqual("Test exception", rows[1][1])
self.assertEqual(2, int(rows[1][0]), "Exception count should be 2")
-
diff --git a/locust/test/testcases.py b/locust/test/testcases.py
index 0b9b261123..21fc6160f7 100644
--- a/locust/test/testcases.py
+++ b/locust/test/testcases.py
@@ -1,16 +1,19 @@
import base64
-import gevent
-import gevent.pywsgi
import random
+import sys
import unittest
+import warnings
from copy import copy
from io import BytesIO
-import sys
+
+import gevent
+import gevent.pywsgi
import six
+from flask import (Flask, Response, make_response, redirect, request,
+ send_file, stream_with_context)
from locust import events
from locust.stats import global_stats
-from flask import Flask, request, redirect, make_response, send_file, Response, stream_with_context
def safe_repr(obj, short=False):
@@ -125,6 +128,17 @@ def setUp(self):
event = getattr(events, name)
if isinstance(event, events.EventHook):
self._event_handlers[event] = copy(event._handlers)
+
+ # When running the tests in Python 3 we get warnings about unclosed sockets.
+ # This causes tests that depends on calls to sys.stderr to fail, so we'll
+ # suppress those warnings. For more info see:
+ # https://github.com/requests/requests/issues/1882
+ try:
+ warnings.filterwarnings(action="ignore", message="unclosed 0:
host = runners.locust_runner.locust_classes[0].host
else:
host = None
-
+
if runners.locust_runner.running_type == runners.NORMAL:
edit_label = "Edit"
else:
@@ -66,6 +65,7 @@ def index():
modules = tests_loader.populate_directories(fileio.os_path(),'tests/modules/')
modules.update(pages)
directories = modules
+ all_directories = tests_loader.populate_directories(fileio.os_path(),'tests/')
return render_template("index.html",
state=runners.locust_runner.state,
@@ -74,6 +74,7 @@ def index():
user_count=runners.locust_runner.user_count,
available_locustfiles = sorted(runners.locust_runner.available_locustfiles.keys()),
test_file_directories = sorted(directories),
+ all_test_file_directories = sorted(all_directories),
version=version,
ramp = _ramp,
host=host
@@ -129,39 +130,11 @@ def stop():
def reset_stats():
runners.locust_runner.stats.reset_all()
return "ok"
-
+
@app.route("/stats/requests/csv")
def request_stats_csv():
- rows = [
- ",".join([
- '"Method"',
- '"Name"',
- '"# requests"',
- '"# failures"',
- '"Median response time"',
- '"Average response time"',
- '"Min response time"',
- '"Max response time"',
- '"Average Content Size"',
- '"Requests/s"',
- ])
- ]
-
- for s in chain(_sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total", full_request_history=True)]):
- rows.append('"%s","%s",%i,%i,%i,%i,%i,%i,%i,%.2f' % (
- s.method,
- s.name,
- s.num_requests,
- s.num_failures,
- s.median_response_time,
- s.avg_response_time,
- s.min_response_time or 0,
- s.max_response_time,
- s.avg_content_length,
- s.total_rps,
- ))
-
- response = make_response("\n".join(rows))
+ response = make_response(requests_csv())
+
file_name = "requests_{0}.csv".format(time())
disposition = "attachment;filename={0}".format(file_name)
response.headers["Content-type"] = "text/csv"
@@ -170,26 +143,8 @@ def request_stats_csv():
@app.route("/stats/distribution/csv")
def distribution_stats_csv():
- rows = [",".join((
- '"Name"',
- '"# requests"',
- '"50%"',
- '"66%"',
- '"75%"',
- '"80%"',
- '"90%"',
- '"95%"',
- '"98%"',
- '"99%"',
- '"100%"',
- ))]
- for s in chain(_sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total", full_request_history=True)]):
- if s.num_requests:
- rows.append(s.percentile(tpl='"%s",%i,%i,%i,%i,%i,%i,%i,%i,%i,%i'))
- else:
- rows.append('"%s",0,"N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A","N/A"' % s.name)
- response = make_response("\n".join(rows))
+ response = make_response(distribution_csv())
file_name = "distribution_{0}.csv".format(time())
disposition = "attachment;filename={0}".format(file_name)
response.headers["Content-type"] = "text/csv"
@@ -200,7 +155,7 @@ def distribution_stats_csv():
@memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
def request_stats():
stats = []
- for s in chain(_sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total")]):
+ for s in chain(sort_stats(runners.locust_runner.request_stats), [runners.locust_runner.stats.aggregated_stats("Total")]):
stats.append({
"method": s.method,
"name": s.name,
@@ -235,14 +190,14 @@ def request_stats():
response_times = defaultdict(int) # used for calculating total median
for i in xrange(len(stats)-1):
response_times[stats[i]["median_response_time"]] += stats[i]["num_requests"]
-
+
# calculate total median
stats[len(stats)-1]["median_response_time"] = median_from_dict(stats[len(stats)-1]["num_requests"], response_times)
-
+
is_distributed = isinstance(runners.locust_runner, MasterLocustRunner)
if is_distributed:
report["slave_count"] = runners.locust_runner.slave_count
-
+
report["state"] = runners.locust_runner.state
report["user_count"] = runners.locust_runner.user_count
report["running_type"] = runners.locust_runner.running_type
@@ -254,9 +209,9 @@ def exceptions():
response = make_response(json.dumps({
'exceptions': [
{
- "count": row["count"],
- "msg": row["msg"],
- "traceback": row["traceback"],
+ "count": row["count"],
+ "msg": row["msg"],
+ "traceback": row["traceback"],
"nodes" : ", ".join(row["nodes"])
} for row in six.itervalues(runners.locust_runner.exceptions)
]
@@ -272,7 +227,7 @@ def exceptions_csv():
for exc in six.itervalues(runners.locust_runner.exceptions):
nodes = ", ".join(exc["nodes"])
writer.writerow([exc["count"], exc["msg"], exc["traceback"], nodes])
-
+
data.seek(0)
response = make_response(data.read())
file_name = "exceptions_{0}.csv".format(time())
@@ -295,7 +250,7 @@ def ramp():
fail_rate = float(int(request.form["fail_rate"]) / 100.0)
calibration_time = int(request.form["wait_time"])
global greenlet_spawner
- greenlet_spawner = gevent.spawn(start_ramping, hatch_rate, max_clients, hatch_stride, percentile, response_time, fail_rate, precision, init_clients, calibration_time)
+ greenlet_spawner = spawn(start_ramping, hatch_rate, max_clients, hatch_stride, percentile, response_time, fail_rate, precision, init_clients, calibration_time)
response = make_response(json.dumps({'success':True, 'message': 'Ramping started'}))
response.headers["Content-type"] = "application/json"
return response
@@ -372,16 +327,18 @@ def convert_csv_to_json():
@app.route("/upload_file", methods=["POST"])
def upload_file():
upload_directory = request.form.get('upload_directory')
- python_file = request.files['python_file']
- python_file_path = upload_directory + python_file.filename
- python_file_extension = os.path.splitext(python_file.filename)[1]
- python_file_content = python_file.read()
- if not python_file and python_file_extension != ".py":
- return expected_response({'success':False, 'message':"Can't upload this file. Please try again with python file with .py extension"})
- upload_status,upload_message = fileio.write(python_file_path, python_file_content)
+ uploaded_file = request.files['uploaded_file']
+ uploaded_file_path = upload_directory + uploaded_file.filename
+ uploaded_file_extension = os.path.splitext(uploaded_file.filename)[1]
+ uploaded_file_content = uploaded_file.read()
+ if not uploaded_file :
+ return expected_response({'success':False, 'message':'Please choose .json or .py file to upload'})
+ if uploaded_file_extension != ".py" and uploaded_file_extension != ".json":
+ return expected_response({'success':False, 'message':"Can't upload this file. Currently only supports .json or .py file"})
+ upload_status,upload_message = fileio.write(uploaded_file_path, uploaded_file_content)
if upload_status is False :
return expected_response({'success':False, 'message':upload_message})
- events.master_new_file_uploaded.fire(new_file={"full_path": python_file_path, "name": python_file.filename, "content":python_file_content})
+ events.master_new_file_uploaded.fire(new_file={"full_path": uploaded_file_path, "name": uploaded_file.filename, "content":uploaded_file_content})
runners.locust_runner.reload_tests()
return expected_response({'success':True, 'message':""})
@@ -408,13 +365,8 @@ def save_json():
def start(locust, options):
global _ramp
_ramp = options.ramp
- wsgi.WSGIServer((options.web_host, options.port), app, log=None).serve_forever()
-
-def _sort_stats(stats):
- return [stats[key] for key in sorted(six.iterkeys(stats))]
-
-def transform(text_file_contents):
- return text_file_contents.replace("=", ",")
+ pywsgi.WSGIServer((options.web_host, options.port),
+ app, log=None).serve_forever()
def send_opsgenie_request(_message):
try:
diff --git a/setup.py b/setup.py
index 1c8805ba4d..e5c4e87c39 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,9 @@
-# encoding: utf-8
-
-from setuptools import setup, find_packages
-import os, re, ast
+# -*- coding: utf-8 -*-
+import ast
+import os
+import re
+from setuptools import find_packages, setup
# parse version from locust/__init__.py
_version_re = re.compile(r'__version__\s+=\s+(.*)')
@@ -41,6 +42,7 @@
include_package_data=True,
zip_safe=False,
install_requires=["pandas==0.22.0","gevent==1.2.2", "flask>=0.10.1", "requests>=2.9.1", "msgpack-python>=0.4.2", "six>=1.10.0", "pyzmq==16.0.4", "jsonpath-rw==1.4.0", "jsonpath-rw-ext==1.1.3", "chardet==3.0.4", "urllib3==1.22"],
+ test_suite="locust.test",
tests_require=['unittest2', 'mock'],
entry_points={
'console_scripts': [