From 95879571ea2559c7caf3654016d23ce2b4a84564 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 10:53:25 +0100 Subject: [PATCH 01/12] Adding docs for programmatic locust. --- docs/index.rst | 1 + docs/running-locust-programmatically.rst | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/running-locust-programmatically.rst diff --git a/docs/index.rst b/docs/index.rst index 1f001a8e2b..962f08ade1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,7 @@ Locust Documentation running-locust-distributed running-locust-without-web-ui + running-locust-programmatically .. toctree :: :maxdepth: 4 diff --git a/docs/running-locust-programmatically.rst b/docs/running-locust-programmatically.rst new file mode 100644 index 0000000000..1cc35480e2 --- /dev/null +++ b/docs/running-locust-programmatically.rst @@ -0,0 +1,17 @@ +.. _running-locust-programmatically: + +=============================== +Running Locust programmatically +=============================== + +All of the options for running locust from the command-line can be used +programmatically within Python using the `locust.run_locust` function. + +`locust.create_options()` will set up an options object with default +values filled in. See below for API reference. + +API Reference +============= + +.. automodule:: locust.main + :members: run_locust, create_options From ba47245407766662fecd8b314e396c3aa50dda2a Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 10:58:05 +0100 Subject: [PATCH 02/12] Allow locust to be run programmatically --- locust/__init__.py | 2 + locust/main.py | 387 +++++++++++++-------------------------------- locust/parser.py | 359 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 467 insertions(+), 281 deletions(-) create mode 100644 locust/parser.py diff --git a/locust/__init__.py b/locust/__init__.py index 7fd2112325..eb032361ef 100644 --- a/locust/__init__.py +++ b/locust/__init__.py @@ -1,4 +1,6 @@ from .core import HttpLocust, Locust, TaskSet, task from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately +from .parser import create_options, parse_options +from .main import run_locust __version__ = "0.8.1" diff --git a/locust/main.py b/locust/main.py index 9a34f03a8b..3b368484b1 100644 --- a/locust/main.py +++ b/locust/main.py @@ -5,14 +5,14 @@ import socket import sys import time -from optparse import OptionParser import gevent import locust from . import events, runners, web -from .core import HttpLocust, Locust +from .parser import parse_options, create_options +from .core import HttpLocust, Locust, TaskSet from .inspectlocust import get_task_ratio_dict, print_task_ratio from .log import console_logger, setup_logging from .runners import LocalLocustRunner, MasterLocustRunner, SlaveLocustRunner @@ -23,252 +23,6 @@ _internals = [Locust, HttpLocust] version = locust.__version__ -def parse_options(): - """ - Handle command-line options with optparse.OptionParser. - - Return list of arguments, largely for use in `parse_arguments`. - """ - - # Initialize - parser = OptionParser(usage="locust [options] [LocustClass [LocustClass2 ... ]]") - - parser.add_option( - '-H', '--host', - dest="host", - default=None, - help="Host to load test in the following format: http://10.21.32.33" - ) - - parser.add_option( - '--web-host', - dest="web_host", - default="", - help="Host to bind the web interface to. Defaults to '' (all interfaces)" - ) - - parser.add_option( - '-P', '--port', '--web-port', - type="int", - dest="port", - default=8089, - help="Port on which to run web host" - ) - - parser.add_option( - '-f', '--locustfile', - dest='locustfile', - default='locustfile', - 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', - action='store_true', - dest='master', - default=False, - help="Set locust to run in distributed mode with this process as master" - ) - - # if locust should be run in distributed mode as slave - parser.add_option( - '--slave', - action='store_true', - dest='slave', - default=False, - help="Set locust to run in distributed mode with this process as slave" - ) - - # master host options - parser.add_option( - '--master-host', - action='store', - type='str', - dest='master_host', - 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', - type='int', - dest='master_port', - default=5557, - help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with --slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." - ) - - parser.add_option( - '--master-bind-host', - action='store', - type='str', - dest='master_bind_host', - 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', - type='int', - dest='master_bind_port', - default=5557, - help="Port that locust master should bind to. Only used when running with --master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." - ) - - parser.add_option( - '--expect-slaves', - action='store', - type='int', - dest='expect_slaves', - default=1, - help="How many slaves master should expect to connect before starting the test (only when --no-web used)." - ) - - # if we should print stats in the console - parser.add_option( - '--no-web', - action='store_true', - dest='no_web', - default=False, - help="Disable the web interface, and instead start running the test immediately. Requires -c and -r to be specified." - ) - - # Number of clients - parser.add_option( - '-c', '--clients', - action='store', - type='int', - dest='num_clients', - default=1, - help="Number of concurrent Locust users. Only used together with --no-web" - ) - - # Client hatch rate - parser.add_option( - '-r', '--hatch-rate', - action='store', - type='float', - dest='hatch_rate', - default=1, - help="The rate per second in which clients are spawned. Only used together with --no-web" - ) - - # Time limit of the test run - parser.add_option( - '-t', '--run-time', - action='store', - type='str', - dest='run_time', - default=None, - help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --no-web" - ) - - # log level - parser.add_option( - '--loglevel', '-L', - action='store', - type='str', - dest='loglevel', - default='INFO', - help="Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO.", - ) - - # log file - parser.add_option( - '--logfile', - action='store', - type='str', - dest='logfile', - 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', - action='store_true', - dest='print_stats', - default=False, - help="Print stats in the console" - ) - - # only print summary stats - parser.add_option( - '--only-summary', - action='store_true', - dest='only_summary', - default=False, - help='Only print the summary stats' - ) - - parser.add_option( - '--no-reset-stats', - action='store_true', - help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See --reset-stats to disable", - ) - - parser.add_option( - '--reset-stats', - action='store_true', - dest='reset_stats', - default=False, - help="Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode", - ) - - # List locust commands found in loaded locust files/source files - parser.add_option( - '-l', '--list', - action='store_true', - dest='list_commands', - default=False, - help="Show list of possible locust classes and exit" - ) - - # Display ratio table of all tasks - parser.add_option( - '--show-task-ratio', - action='store_true', - dest='show_task_ratio', - default=False, - help="print table of the locust classes' task execution ratio" - ) - # Display ratio table of all tasks in JSON format - parser.add_option( - '--show-task-ratio-json', - action='store_true', - dest='show_task_ratio_json', - 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( - '-V', '--version', - action='store_true', - dest='show_version', - default=False, - help="show program's version number and exit" - ) - - # Finalize - # Return three-tuple of parser + the output from parse_args (opt obj, args) - opts, args = parser.parse_args() - return parser, opts, args - - def _is_package(path): """ Is the given path a Python package? @@ -283,6 +37,8 @@ def find_locustfile(locustfile): """ Attempt to locate a locustfile, either explicitly or by searching parent dirs. """ + if locustfile is None: + return None # Obtain env value names = [locustfile] # Create .py version if necessary @@ -313,7 +69,7 @@ def find_locustfile(locustfile): # Implicit 'return None' if nothing was found -def is_locust(tup): +def is_locust(tup, ignore_prefix='_'): """ Takes (name, object) tuple, returns True if it's a public Locust subclass. """ @@ -323,20 +79,32 @@ def is_locust(tup): and issubclass(item, Locust) and hasattr(item, "task_set") and getattr(item, "task_set") - and not name.startswith('_') + and not name.startswith(ignore_prefix) ) -def load_locustfile(path): +def is_taskset(tup, ignore_prefix='_'): + """Takes (name, object) tuple, returns True if it's a public TaskSet subclass.""" + name, item = tup + return bool( + inspect.isclass(item) + and issubclass(item, TaskSet) + and hasattr(item, "tasks") + and not name.startswith(ignore_prefix) + and name != 'TaskSet' + ) + + +def load_filterfile(path, filter_function): """ - Import given locustfile path and return (docstring, callables). + Import given path and return (docstring, callables) of all clases that pass the test. - Specifically, the locustfile's ``__doc__`` attribute (a string) and a + Specifically, the classfile's ``__doc__`` attribute (a string) and a dictionary of ``{'name': callable}`` containing all callables which pass - the "is a Locust" test. + the `filter_function` test. """ # Get directory and locustfile name - directory, locustfile = os.path.split(path) + directory, filterfile = os.path.split(path) # If the directory isn't in the PYTHONPATH, add it so our import will work added_to_path = False index = None @@ -355,7 +123,7 @@ def load_locustfile(path): sys.path.insert(0, directory) del sys.path[i + 1] # Perform the import (trimming off the .py) - imported = __import__(os.path.splitext(locustfile)[0]) + imported = __import__(os.path.splitext(filterfile)[0]) # Remove directory from path if we added it ourselves (just to be neat) if added_to_path: del sys.path[0] @@ -364,48 +132,100 @@ def load_locustfile(path): sys.path.insert(index + 1, directory) del sys.path[0] # Return our two-tuple - locusts = dict(filter(is_locust, vars(imported).items())) - return imported.__doc__, locusts + classes = dict(filter(filter_function, vars(imported).items())) + return imported.__doc__, classes -def main(): - parser, options, arguments = parse_options() +def load_locustfile(path): + """ + Import given locustfile path and return (docstring, callables). + + Specifically, the locustfile's ``__doc__`` attribute (a string) and a + dictionary of ``{'name': callable}`` containing all callables which pass + the "is a Locust" test. + """ + return load_filterfile(path, is_locust) + + +def load_tasksetfile(path): + """ + Import given tasksetfile path and return (docstring, callables). + + Specifically, the tasksetfile's ``__doc__`` attribute (a string) and a + dictionary of ``{'name': callable}`` containing all callables which pass + the "is a TaskSet" test (`is_taskset`). + """ + return load_filterfile(path, is_taskset) + +default_options = create_options() + +def run_locust(options, arguments=[], cli_mode=False): + """Run Locust programmatically. + + A default set of options can be acquired using the `create_options` function with no arguments. + + Arguments: + options (OptionParser): OptionParser object for defining Locust options. Obtain + by either using the `create_options` function or running `parse_options` on + an argv-style list using the locust command-line format. Recommended to use + `create_options`. + arguments (list)): List of Locust classes to include from those found. + """ # setup logging setup_logging(options.loglevel, options.logfile) logger = logging.getLogger(__name__) + + def locust_error(message, err_type=ValueError, exit_type=1): + logger.error(message) + if not cli_mode: + raise err_type(message) + sys.exit(exit_type) if options.show_version: - print("Locust %s" % (version,)) + version_text = "Locust %s" % (version,) + print(version_text) + if not cli_mode: + return 0 sys.exit(0) + # Either there is a locustfile, there are locust_classes, or both. locustfile = find_locustfile(options.locustfile) - if not locustfile: - logger.error("Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.") - sys.exit(1) + if not hasattr(options,'locust_classes'): + options.locust_classes=[] - if locustfile == "locust.py": - logger.error("The locustfile must not be named `locust.py`. Please rename the file and try again.") - sys.exit(1) + locusts = {} + if locustfile: + if locustfile == "locust.py": + locust_error("The locustfile must not be named `locust.py`. Please rename the file and try again.") + docstring, locusts = load_locustfile(locustfile) + elif not options.locust_classes: + locust_error("Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.") + else: + pass - docstring, locusts = load_locustfile(locustfile) + for x in options.locust_classes: + name = x.__name__ + if name in locusts: + locust_error("Duplicate locust name {}.".format(name)) + locusts[name] = x if options.list_commands: console_logger.info("Available Locusts:") for name in locusts: console_logger.info(" " + name) + if not cli_mode: + return [name for name in locusts] sys.exit(0) if not locusts: - logger.error("No Locust class found!") - sys.exit(1) + locust_error("No Locust class found!") # make sure specified Locust exists if arguments: missing = set(arguments) - set(locusts.keys()) if missing: - logger.error("Unknown Locust(s): %s\n" % (", ".join(missing))) - sys.exit(1) + locust_error("Unknown Locust(s): %s\n" % (", ".join(missing))) else: names = set(arguments) & set(locusts.keys()) locust_classes = [locusts[n] for n in names] @@ -420,6 +240,8 @@ def main(): console_logger.info("\n Total task ratio") console_logger.info("-" * 80) print_task_ratio(locust_classes, total=True) + if not cli_mode: + return 0 sys.exit(0) if options.show_task_ratio_json: from json import dumps @@ -432,13 +254,11 @@ def main(): if options.run_time: if not options.no_web: - logger.error("The --run-time argument can only be used together with --no-web") - sys.exit(1) + locust_error("The --run-time argument can only be used together with --no-web") try: options.run_time = parse_timespan(options.run_time) except ValueError: - logger.error("Valid --run-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.") - sys.exit(1) + locust_error("Valid --run-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.") def spawn_run_time_limit_greenlet(): logger.info("Run time limit set to %s seconds" % options.run_time) def timelimit_stop(): @@ -473,14 +293,13 @@ def timelimit_stop(): spawn_run_time_limit_greenlet() elif options.slave: if options.run_time: - logger.error("--run-time should be specified on the master node, and not on slave nodes") - sys.exit(1) + locust_error("--run-time should be specified on the master node, and not on slave nodes") try: runners.locust_runner = SlaveLocustRunner(locust_classes, options) main_greenlet = runners.locust_runner.greenlet except socket.error as e: - logger.error("Failed to connect to the Locust master: %s", e) - sys.exit(-1) + socket_err = "Failed to connect to the Locust master: {}".format(e) + locust_error(socket_err, err_type=socket.error, exit_type=-1) if not options.only_summary and (options.print_stats or (options.no_web and not options.slave)): # spawn stats printing greenlet @@ -489,7 +308,7 @@ def timelimit_stop(): if options.csvfilebase: gevent.spawn(stats_writer, options.csvfilebase) - + def shutdown(code=0): """ Shut down locust by firing quitting event, printing/writing stats and exiting @@ -524,5 +343,11 @@ def sig_term_handler(): except KeyboardInterrupt as e: shutdown(0) + +def main(): + _, options, arguments = parse_options(sys.argv) + args = arguments[1:] + run_locust(options=options, arguments=args, cli_mode=True) + if __name__ == '__main__': main() diff --git a/locust/parser.py b/locust/parser.py new file mode 100644 index 0000000000..973c5b5b9f --- /dev/null +++ b/locust/parser.py @@ -0,0 +1,359 @@ +from optparse import OptionParser +import sys + +def create_parser(): + """Create parser object used for defining all options for Locust. + + Returns: + OptionParser: OptionParser object used in *parse_options*. + """ + + # Initialize + parser = OptionParser(usage="locust [options] [LocustClass [LocustClass2 ... ]]") + + parser.add_option( + '-H', '--host', + dest="host", + default=None, + help="Host to load test in the following format: http://10.21.32.33" + ) + + parser.add_option( + '--web-host', + dest="web_host", + default="", + help="Host to bind the web interface to. Defaults to '' (all interfaces)" + ) + + parser.add_option( + '-P', '--port', '--web-port', + type="int", + dest="port", + default=8089, + help="Port on which to run web host" + ) + + parser.add_option( + '-f', '--locustfile', + dest='locustfile', + default='locustfile', + help="Python module file to import, e.g. '../other.py'. Default: locustfilePython 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', + action='store_true', + dest='master', + default=False, + help="Set locust to run in distributed mode with this process as master" + ) + + # if locust should be run in distributed mode as slave + parser.add_option( + '--slave', + action='store_true', + dest='slave', + default=False, + help="Set locust to run in distributed mode with this process as slave" + ) + + # master host options + parser.add_option( + '--master-host', + action='store', + type='str', + dest='master_host', + 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', + type='int', + dest='master_port', + default=5557, + help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." + ) + + parser.add_option( + '--master-bind-host', + action='store', + type='str', + dest='master_bind_host', + 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', + type='int', + dest='master_bind_port', + default=5557, + help="Port that locust master should bind to. Only used when running with:master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." + ) + + parser.add_option( + '--expect-slaves', + action='store', + type='int', + dest='expect_slaves', + default=1, + help="How many slaves master should expect to connect before starting the test (only when:no-web used)." + ) + + # if we should print stats in the console + parser.add_option( + '--no-web', + action='store_true', + dest='no_web', + default=False, + help="Disable the web interface, and instead start running the test immediately. Requires -c and -r to be specified." + ) + + # Number of clients + parser.add_option( + '-c', '--clients', + action='store', + type='int', + dest='num_clients', + default=1, + help="Number of concurrent Locust users. Only used together with:no-web" + ) + + # Client hatch rate + parser.add_option( + '-r', '--hatch-rate', + action='store', + type='float', + dest='hatch_rate', + default=1, + help="The rate per second in which clients are spawned. Only used together with:no-web" + ) + + # Time limit of the test run + parser.add_option( + '-t', '--run-time', + action='store', + type='str', + dest='run_time', + default=None, + help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web" + ) + + # log level + parser.add_option( + '--loglevel', '-L', + action='store', + type='str', + dest='loglevel', + default='INFO', + help="Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO.", + ) + + # log file + parser.add_option( + '--logfile', + action='store', + type='str', + dest='logfile', + 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', + action='store_true', + dest='print_stats', + default=False, + help="Print stats in the console" + ) + + # only print summary stats + parser.add_option( + '--only-summary', + action='store_true', + dest='only_summary', + default=False, + help="Only print the summary stats" + ) + + parser.add_option( + '--no-reset-stats', + action='store_true', + help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See:reset-stats to disable", + ) + + parser.add_option( + '--reset-stats', + action='store_true', + dest='reset_stats', + default=False, + help="Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode", + ) + + # List locust commands found in loaded locust files/source files + parser.add_option( + '-l', '--list', + action='store_true', + dest='list_commands', + default=False, + help="Show list of possible locust classes and exit" + ) + + # Display ratio table of all tasks + parser.add_option( + '--show-task-ratio', + action='store_true', + dest='show_task_ratio', + default=False, + help="print table of the locust classes' task execution ratio" + ) + # Display ratio table of all tasks in JSON format + parser.add_option( + '--show-task-ratio-json', + action='store_true', + dest='show_task_ratio_json', + 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( + '-V', '--version', + action='store_true', + dest='show_version', + default=False, + help="show program's version number and exit" + ) + return parser + + +def create_options( + locustfile='locustfile', + host=None, + locust_classes=[], + port=8089, + web_host='', + no_reset_stats=None, + reset_stats=False, + no_web=False, + run_time=None, + num_clients=1, + hatch_rate=1, + master=False, + expect_slaves=1, + master_bind_host='*', + master_bind_port=5557, + slave=False, + master_host='127.0.0.1', + master_port=5557, + csvfilebase=None, + print_stats=False, + only_summary=False, + show_task_ratio=False, + show_task_ratio_json=False, + logfile=None, + loglevel='INFO', + show_version=False, + list_commands=False): + """Create options objects for passing to `run_locust` when running Locust programmatically. + + Keyword Arguments: + locustfile (str): Python module file to import, e.g. '../other.py'. (default 'locustfile') + host (str): Host to load test in the following format: http://10.21.32.33 (default: None) + locust_classes (list): Locust class callables if not importing from file (default: []) + port (int): Port on which to run web host (default: 8089) + web_host (str): Host to bind the web interface to. (default: '' (all interfaces)) + reset_stats (bool): Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode (default: False) + no_web (bool): Disable the web interface, and instead start running the test immediately. Requires num_clients and hatch_rate to be specified. (default: False) + run_time (str): Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web (default: None) + num_clients (int): Number of concurrent Locust users. Only used together with no_web (default: 1) + hatch_rate (int): The rate per second in which clients are spawned. Only used together with:no-web (default: 1) + master (bool): Set locust to run in distributed mode with this process as master (default: False) + expect_slaves (int): How many slaves master should expect to connect before starting the test (only when no_web used). (default: 1) + master_bind_host (str): Interfaces (hostname, ip) that locust master should bind to. Only used when running with master. Defaults all available interfaces. (default: '*') + master_bind_port (int): Port that locust master should bind to. Only used when running with:master. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558. (default: 5557) + slave (bool): Set locust to run in distributed mode with this process as slave (default: False) + master_host (str): Host or IP address of locust master for distributed load testing. Only used when running with slave. (default: '127.0.0.1') + master_port (int): The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Note that slaves will also connect to the master node on this port + 1. (default: 5557) + csvfilebase (str): Store current request stats to files in CSV format. (default: None) + print_stats (bool): Print stats in the console (default: False) + only_summary (bool): Only print the summary stats (default: False) + show_task_ratio (bool): print table of the locust classes' task execution ratio (default: False) + show_task_ratio_json (bool): print json data of the locust classes' task execution ratio (default: False) + logfile (str): Path to log file. If not set, log will go to stdout/stderr (default: None) + loglevel (str): Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. (default: 'INFO') + show_version (bool): show program's version number and exit (default: False) + list_commands (bool): Return list of possible locust classes and exit (default: False) + + """ + + + opts,_ = create_parser().parse_args([]) + opts.locustfile = locustfile + opts.host=host + opts.locust_classes=locust_classes # Locust class {name:callables, ...} if not loading from file path + + # web interface + opts.port=port + opts.web_host=web_host + + # No web + opts.no_web=no_web + opts.run_time=run_time + opts.num_clients=num_clients + opts.hatch_rate=hatch_rate + + # Distributed settings + # Master settings + opts.master=master + opts.expect_slaves=expect_slaves + opts.master_bind_host=master_bind_host + opts.master_bind_port=master_bind_port + # Slave settings + opts.slave=slave + opts.master_host=master_host + opts.master_port=master_port + + # Output settings + opts.reset_stats=reset_stats + opts.csvfilebase=csvfilebase + opts.print_stats=print_stats + opts.only_summary=only_summary + opts.show_task_ratio=show_task_ratio + opts.show_task_ratio_json=show_task_ratio_json + opts.logfile=logfile + opts.loglevel=loglevel + + # Miscellaneous + opts.show_version=show_version + opts.list_commands=list_commands + + return opts + + +def parse_options(args=sys.argv): + """ + Handle command-line options with optparse.OptionParser. + + Return list of arguments, largely for use in `parse_arguments`. + """ + parser = create_parser() + # Return three-tuple of parser + the output from parse_args (opt obj, args) + opts, args = parser.parse_args(args) + return parser, opts, args \ No newline at end of file From c4334f59e5f2cd4005ad467eaeb3b554171548c5 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 10:58:34 +0100 Subject: [PATCH 03/12] Adding example of programmatic functionality --- examples/run_programmatically.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 examples/run_programmatically.py diff --git a/examples/run_programmatically.py b/examples/run_programmatically.py new file mode 100644 index 0000000000..edd49a0551 --- /dev/null +++ b/examples/run_programmatically.py @@ -0,0 +1,28 @@ +from locust import HttpLocust, TaskSet, task, create_options, run_locust + +def index(l): + l.client.get("/") + +def stats(l): + l.client.get("/stats/requests") + +class UserTasks(TaskSet): + # one can specify tasks like this + tasks = [index, stats] + + # but it might be convenient to use the @task decorator + @task + def page404(self): + self.client.get("/does_not_exist") + +class WebsiteUser(HttpLocust): + """ + Locust user class that does requests to the locust web server running on localhost + """ + host = "http://127.0.0.1:8089" + min_wait = 2000 + max_wait = 5000 + task_set = UserTasks + +options = create_options(locust_classes = [WebsiteUser]) +run_locust(options) From 159b8705999cc9bad6fa5c829f73308e78c533ac Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 10:59:16 +0100 Subject: [PATCH 04/12] Adding tests for programmatic locust --- locust/test/test_main.py | 51 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 6686c5d281..791b915964 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -6,11 +6,6 @@ class TestTaskSet(LocustTestCase): def test_is_locust(self): - self.assertFalse(main.is_locust(("Locust", Locust))) - self.assertFalse(main.is_locust(("HttpLocust", HttpLocust))) - self.assertFalse(main.is_locust(("random_dict", {}))) - self.assertFalse(main.is_locust(("random_list", []))) - class MyTaskSet(TaskSet): pass @@ -19,6 +14,11 @@ class MyHttpLocust(HttpLocust): class MyLocust(Locust): task_set = MyTaskSet + + self.assertFalse(main.is_locust(("Locust", Locust))) + self.assertFalse(main.is_locust(("HttpLocust", HttpLocust))) + self.assertFalse(main.is_locust(("random_dict", {}))) + self.assertFalse(main.is_locust(("random_list", []))) self.assertTrue(main.is_locust(("MyHttpLocust", MyHttpLocust))) self.assertTrue(main.is_locust(("MyLocust", MyLocust))) @@ -27,3 +27,44 @@ class ThriftLocust(Locust): pass self.assertFalse(main.is_locust(("ThriftLocust", ThriftLocust))) + + def test_run_programmatically(self): + class MyTaskSet(TaskSet): + pass + + class MyHttpLocust(HttpLocust): + task_set = MyTaskSet + + class MyLocust(Locust): + task_set = MyTaskSet + + default_options = main.create_options(list_commands=True, locustfile="locustfile_test.py") + # Test that it runs with locustfile + self.assertEqual(set(main.run_locust(default_options)), set(["LocustfileHttpLocust","LocustfileLocust"])) + + # Test locustfile conflicts with locust_classes + class LocustfileLocust(Locust): + task_set = MyTaskSet + conflict_options = default_options + conflict_options.locust_classes=[LocustfileLocust] + self.assertRaises(ValueError,lambda: main.run_locust(conflict_options)) + + # Test that it runs with locustfile and locust options + locustfile_and_classes = default_options + locustfile_and_classes.locust_classes=[MyLocust, MyHttpLocust] + + self.assertEqual( + set(main.run_locust(locustfile_and_classes)), + set(["LocustfileHttpLocust", "LocustfileLocust", "MyLocust", "MyHttpLocust"])) + + # Test that it runs without locustfile + no_locustfile = locustfile_and_classes + no_locustfile.locustfile=None + main.run_locust(no_locustfile) + self.assertEqual(set(main.run_locust(no_locustfile)), set(["MyLocust", "MyHttpLocust"])) + + # Test that arguments and locust_classes work correctly + self.assertEqual(set(main.run_locust(no_locustfile, arguments=["MyLocust"])),set(["MyLocust", "MyHttpLocust"])) + + + From 5ddff2870c5ed661998ef352aa9a8a812278482a Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 11:01:05 +0100 Subject: [PATCH 05/12] move version to top of file to fix cannot import name version error --- locust/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locust/__init__.py b/locust/__init__.py index eb032361ef..6f0198c54d 100644 --- a/locust/__init__.py +++ b/locust/__init__.py @@ -1,6 +1,7 @@ +__version__ = "0.8.1" + from .core import HttpLocust, Locust, TaskSet, task from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately from .parser import create_options, parse_options from .main import run_locust -__version__ = "0.8.1" From 54944f4047a4c306ad8f486f3692ccaa73ba831d Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 28 May 2018 11:05:39 +0100 Subject: [PATCH 06/12] Placing locustfile for testing in test/ --- locust/test/locustfile_test.py | 10 ++++++++++ locust/test/test_main.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 locust/test/locustfile_test.py diff --git a/locust/test/locustfile_test.py b/locust/test/locustfile_test.py new file mode 100644 index 0000000000..79ac31762e --- /dev/null +++ b/locust/test/locustfile_test.py @@ -0,0 +1,10 @@ +from locust import HttpLocust, Locust, TaskSet + +class MyTaskSet(TaskSet): + pass + +class LocustfileHttpLocust(HttpLocust): + task_set = MyTaskSet + +class LocustfileLocust(Locust): + task_set = MyTaskSet \ No newline at end of file diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 791b915964..677f111081 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -38,7 +38,7 @@ class MyHttpLocust(HttpLocust): class MyLocust(Locust): task_set = MyTaskSet - default_options = main.create_options(list_commands=True, locustfile="locustfile_test.py") + default_options = main.create_options(list_commands=True, locustfile="locust/test/locustfile_test.py") # Test that it runs with locustfile self.assertEqual(set(main.run_locust(default_options)), set(["LocustfileHttpLocust","LocustfileLocust"])) From bc7d517b7219a382a2656d2459d59639ccad6f65 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Tue, 29 May 2018 14:23:21 +0100 Subject: [PATCH 07/12] Remove parser split --- locust/__init__.py | 3 +- locust/main.py | 359 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 359 insertions(+), 3 deletions(-) diff --git a/locust/__init__.py b/locust/__init__.py index 6f0198c54d..2636880d40 100644 --- a/locust/__init__.py +++ b/locust/__init__.py @@ -2,6 +2,5 @@ from .core import HttpLocust, Locust, TaskSet, task from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately -from .parser import create_options, parse_options -from .main import run_locust +from .main import run_locust, create_options, parse_options diff --git a/locust/main.py b/locust/main.py index 3b368484b1..b38d0315ff 100644 --- a/locust/main.py +++ b/locust/main.py @@ -5,13 +5,13 @@ import socket import sys import time +from optparse import OptionParser import gevent import locust from . import events, runners, web -from .parser import parse_options, create_options from .core import HttpLocust, Locust, TaskSet from .inspectlocust import get_task_ratio_dict, print_task_ratio from .log import console_logger, setup_logging @@ -23,6 +23,363 @@ _internals = [Locust, HttpLocust] version = locust.__version__ +def create_parser(): + """Create parser object used for defining all options for Locust. + + Returns: + OptionParser: OptionParser object used in *parse_options*. + """ + + # Initialize + parser = OptionParser(usage="locust [options] [LocustClass [LocustClass2 ... ]]") + + parser.add_option( + '-H', '--host', + dest="host", + default=None, + help="Host to load test in the following format: http://10.21.32.33" + ) + + parser.add_option( + '--web-host', + dest="web_host", + default="", + help="Host to bind the web interface to. Defaults to '' (all interfaces)" + ) + + parser.add_option( + '-P', '--port', '--web-port', + type="int", + dest="port", + default=8089, + help="Port on which to run web host" + ) + + parser.add_option( + '-f', '--locustfile', + dest='locustfile', + default='locustfile', + help="Python module file to import, e.g. '../other.py'. Default: locustfilePython 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', + action='store_true', + dest='master', + default=False, + help="Set locust to run in distributed mode with this process as master" + ) + + # if locust should be run in distributed mode as slave + parser.add_option( + '--slave', + action='store_true', + dest='slave', + default=False, + help="Set locust to run in distributed mode with this process as slave" + ) + + # master host options + parser.add_option( + '--master-host', + action='store', + type='str', + dest='master_host', + 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', + type='int', + dest='master_port', + default=5557, + help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." + ) + + parser.add_option( + '--master-bind-host', + action='store', + type='str', + dest='master_bind_host', + 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', + type='int', + dest='master_bind_port', + default=5557, + help="Port that locust master should bind to. Only used when running with:master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." + ) + + parser.add_option( + '--expect-slaves', + action='store', + type='int', + dest='expect_slaves', + default=1, + help="How many slaves master should expect to connect before starting the test (only when:no-web used)." + ) + + # if we should print stats in the console + parser.add_option( + '--no-web', + action='store_true', + dest='no_web', + default=False, + help="Disable the web interface, and instead start running the test immediately. Requires -c and -r to be specified." + ) + + # Number of clients + parser.add_option( + '-c', '--clients', + action='store', + type='int', + dest='num_clients', + default=1, + help="Number of concurrent Locust users. Only used together with:no-web" + ) + + # Client hatch rate + parser.add_option( + '-r', '--hatch-rate', + action='store', + type='float', + dest='hatch_rate', + default=1, + help="The rate per second in which clients are spawned. Only used together with:no-web" + ) + + # Time limit of the test run + parser.add_option( + '-t', '--run-time', + action='store', + type='str', + dest='run_time', + default=None, + help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web" + ) + + # log level + parser.add_option( + '--loglevel', '-L', + action='store', + type='str', + dest='loglevel', + default='INFO', + help="Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO.", + ) + + # log file + parser.add_option( + '--logfile', + action='store', + type='str', + dest='logfile', + 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', + action='store_true', + dest='print_stats', + default=False, + help="Print stats in the console" + ) + + # only print summary stats + parser.add_option( + '--only-summary', + action='store_true', + dest='only_summary', + default=False, + help="Only print the summary stats" + ) + + parser.add_option( + '--no-reset-stats', + action='store_true', + help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See:reset-stats to disable", + ) + + parser.add_option( + '--reset-stats', + action='store_true', + dest='reset_stats', + default=False, + help="Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode", + ) + + # List locust commands found in loaded locust files/source files + parser.add_option( + '-l', '--list', + action='store_true', + dest='list_commands', + default=False, + help="Show list of possible locust classes and exit" + ) + + # Display ratio table of all tasks + parser.add_option( + '--show-task-ratio', + action='store_true', + dest='show_task_ratio', + default=False, + help="print table of the locust classes' task execution ratio" + ) + # Display ratio table of all tasks in JSON format + parser.add_option( + '--show-task-ratio-json', + action='store_true', + dest='show_task_ratio_json', + 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( + '-V', '--version', + action='store_true', + dest='show_version', + default=False, + help="show program's version number and exit" + ) + return parser + + +def create_options( + locustfile='locustfile', + host=None, + locust_classes=[], + port=8089, + web_host='', + no_reset_stats=None, + reset_stats=False, + no_web=False, + run_time=None, + num_clients=1, + hatch_rate=1, + master=False, + expect_slaves=1, + master_bind_host='*', + master_bind_port=5557, + slave=False, + master_host='127.0.0.1', + master_port=5557, + csvfilebase=None, + print_stats=False, + only_summary=False, + show_task_ratio=False, + show_task_ratio_json=False, + logfile=None, + loglevel='INFO', + show_version=False, + list_commands=False): + """Create options objects for passing to `run_locust` when running Locust programmatically. + + Keyword Arguments: + locustfile (str): Python module file to import, e.g. '../other.py'. (default 'locustfile') + host (str): Host to load test in the following format: http://10.21.32.33 (default: None) + locust_classes (list): Locust class callables if not importing from file (default: []) + port (int): Port on which to run web host (default: 8089) + web_host (str): Host to bind the web interface to. (default: '' (all interfaces)) + reset_stats (bool): Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode (default: False) + no_web (bool): Disable the web interface, and instead start running the test immediately. Requires num_clients and hatch_rate to be specified. (default: False) + run_time (str): Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web (default: None) + num_clients (int): Number of concurrent Locust users. Only used together with no_web (default: 1) + hatch_rate (int): The rate per second in which clients are spawned. Only used together with:no-web (default: 1) + master (bool): Set locust to run in distributed mode with this process as master (default: False) + expect_slaves (int): How many slaves master should expect to connect before starting the test (only when no_web used). (default: 1) + master_bind_host (str): Interfaces (hostname, ip) that locust master should bind to. Only used when running with master. Defaults all available interfaces. (default: '*') + master_bind_port (int): Port that locust master should bind to. Only used when running with:master. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558. (default: 5557) + slave (bool): Set locust to run in distributed mode with this process as slave (default: False) + master_host (str): Host or IP address of locust master for distributed load testing. Only used when running with slave. (default: '127.0.0.1') + master_port (int): The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Note that slaves will also connect to the master node on this port + 1. (default: 5557) + csvfilebase (str): Store current request stats to files in CSV format. (default: None) + print_stats (bool): Print stats in the console (default: False) + only_summary (bool): Only print the summary stats (default: False) + show_task_ratio (bool): print table of the locust classes' task execution ratio (default: False) + show_task_ratio_json (bool): print json data of the locust classes' task execution ratio (default: False) + logfile (str): Path to log file. If not set, log will go to stdout/stderr (default: None) + loglevel (str): Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. (default: 'INFO') + show_version (bool): show program's version number and exit (default: False) + list_commands (bool): Return list of possible locust classes and exit (default: False) + + """ + + + opts,_ = create_parser().parse_args([]) + opts.locustfile = locustfile + opts.host=host + opts.locust_classes=locust_classes # Locust class {name:callables, ...} if not loading from file path + + # web interface + opts.port=port + opts.web_host=web_host + + # No web + opts.no_web=no_web + opts.run_time=run_time + opts.num_clients=num_clients + opts.hatch_rate=hatch_rate + + # Distributed settings + # Master settings + opts.master=master + opts.expect_slaves=expect_slaves + opts.master_bind_host=master_bind_host + opts.master_bind_port=master_bind_port + # Slave settings + opts.slave=slave + opts.master_host=master_host + opts.master_port=master_port + + # Output settings + opts.reset_stats=reset_stats + opts.csvfilebase=csvfilebase + opts.print_stats=print_stats + opts.only_summary=only_summary + opts.show_task_ratio=show_task_ratio + opts.show_task_ratio_json=show_task_ratio_json + opts.logfile=logfile + opts.loglevel=loglevel + + # Miscellaneous + opts.show_version=show_version + opts.list_commands=list_commands + + return opts + + +def parse_options(args=sys.argv): + """ + Handle command-line options with optparse.OptionParser. + + Return list of arguments, largely for use in `parse_arguments`. + """ + parser = create_parser() + # Return three-tuple of parser + the output from parse_args (opt obj, args) + opts, args = parser.parse_args(args) + return parser, opts, args + def _is_package(path): """ Is the given path a Python package? From d391fa9d11ea723cc57b54dbfee839497508643d Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Tue, 29 May 2018 14:26:47 +0100 Subject: [PATCH 08/12] Remove parser, fix typos in option list --- locust/main.py | 12 +- locust/parser.py | 359 ----------------------------------------------- 2 files changed, 6 insertions(+), 365 deletions(-) delete mode 100644 locust/parser.py diff --git a/locust/main.py b/locust/main.py index b38d0315ff..5c207cbd5f 100644 --- a/locust/main.py +++ b/locust/main.py @@ -59,7 +59,7 @@ def create_parser(): '-f', '--locustfile', dest='locustfile', default='locustfile', - help="Python module file to import, e.g. '../other.py'. Default: locustfilePython module file to import, e.g. '../other.py'. Default: locustfile" + help="Python module file to import, e.g. '../other.py'. Default: locustfile" ) # A file that contains the current request stats. @@ -133,7 +133,7 @@ def create_parser(): type='int', dest='expect_slaves', default=1, - help="How many slaves master should expect to connect before starting the test (only when:no-web used)." + help="How many slaves master should expect to connect before starting the test (only when --no-web used)." ) # if we should print stats in the console @@ -152,7 +152,7 @@ def create_parser(): type='int', dest='num_clients', default=1, - help="Number of concurrent Locust users. Only used together with:no-web" + help="Number of concurrent Locust users. Only used together with --no-web" ) # Client hatch rate @@ -162,7 +162,7 @@ def create_parser(): type='float', dest='hatch_rate', default=1, - help="The rate per second in which clients are spawned. Only used together with:no-web" + help="The rate per second in which clients are spawned. Only used together with --no-web" ) # Time limit of the test run @@ -172,7 +172,7 @@ def create_parser(): type='str', dest='run_time', default=None, - help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web" + help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --no-web" ) # log level @@ -216,7 +216,7 @@ def create_parser(): parser.add_option( '--no-reset-stats', action='store_true', - help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See:reset-stats to disable", + help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See --reset-stats to disable", ) parser.add_option( diff --git a/locust/parser.py b/locust/parser.py deleted file mode 100644 index 973c5b5b9f..0000000000 --- a/locust/parser.py +++ /dev/null @@ -1,359 +0,0 @@ -from optparse import OptionParser -import sys - -def create_parser(): - """Create parser object used for defining all options for Locust. - - Returns: - OptionParser: OptionParser object used in *parse_options*. - """ - - # Initialize - parser = OptionParser(usage="locust [options] [LocustClass [LocustClass2 ... ]]") - - parser.add_option( - '-H', '--host', - dest="host", - default=None, - help="Host to load test in the following format: http://10.21.32.33" - ) - - parser.add_option( - '--web-host', - dest="web_host", - default="", - help="Host to bind the web interface to. Defaults to '' (all interfaces)" - ) - - parser.add_option( - '-P', '--port', '--web-port', - type="int", - dest="port", - default=8089, - help="Port on which to run web host" - ) - - parser.add_option( - '-f', '--locustfile', - dest='locustfile', - default='locustfile', - help="Python module file to import, e.g. '../other.py'. Default: locustfilePython 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', - action='store_true', - dest='master', - default=False, - help="Set locust to run in distributed mode with this process as master" - ) - - # if locust should be run in distributed mode as slave - parser.add_option( - '--slave', - action='store_true', - dest='slave', - default=False, - help="Set locust to run in distributed mode with this process as slave" - ) - - # master host options - parser.add_option( - '--master-host', - action='store', - type='str', - dest='master_host', - 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', - type='int', - dest='master_port', - default=5557, - help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." - ) - - parser.add_option( - '--master-bind-host', - action='store', - type='str', - dest='master_bind_host', - 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', - type='int', - dest='master_bind_port', - default=5557, - help="Port that locust master should bind to. Only used when running with:master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." - ) - - parser.add_option( - '--expect-slaves', - action='store', - type='int', - dest='expect_slaves', - default=1, - help="How many slaves master should expect to connect before starting the test (only when:no-web used)." - ) - - # if we should print stats in the console - parser.add_option( - '--no-web', - action='store_true', - dest='no_web', - default=False, - help="Disable the web interface, and instead start running the test immediately. Requires -c and -r to be specified." - ) - - # Number of clients - parser.add_option( - '-c', '--clients', - action='store', - type='int', - dest='num_clients', - default=1, - help="Number of concurrent Locust users. Only used together with:no-web" - ) - - # Client hatch rate - parser.add_option( - '-r', '--hatch-rate', - action='store', - type='float', - dest='hatch_rate', - default=1, - help="The rate per second in which clients are spawned. Only used together with:no-web" - ) - - # Time limit of the test run - parser.add_option( - '-t', '--run-time', - action='store', - type='str', - dest='run_time', - default=None, - help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web" - ) - - # log level - parser.add_option( - '--loglevel', '-L', - action='store', - type='str', - dest='loglevel', - default='INFO', - help="Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. Default is INFO.", - ) - - # log file - parser.add_option( - '--logfile', - action='store', - type='str', - dest='logfile', - 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', - action='store_true', - dest='print_stats', - default=False, - help="Print stats in the console" - ) - - # only print summary stats - parser.add_option( - '--only-summary', - action='store_true', - dest='only_summary', - default=False, - help="Only print the summary stats" - ) - - parser.add_option( - '--no-reset-stats', - action='store_true', - help="[DEPRECATED] Do not reset statistics once hatching has been completed. This is now the default behavior. See:reset-stats to disable", - ) - - parser.add_option( - '--reset-stats', - action='store_true', - dest='reset_stats', - default=False, - help="Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode", - ) - - # List locust commands found in loaded locust files/source files - parser.add_option( - '-l', '--list', - action='store_true', - dest='list_commands', - default=False, - help="Show list of possible locust classes and exit" - ) - - # Display ratio table of all tasks - parser.add_option( - '--show-task-ratio', - action='store_true', - dest='show_task_ratio', - default=False, - help="print table of the locust classes' task execution ratio" - ) - # Display ratio table of all tasks in JSON format - parser.add_option( - '--show-task-ratio-json', - action='store_true', - dest='show_task_ratio_json', - 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( - '-V', '--version', - action='store_true', - dest='show_version', - default=False, - help="show program's version number and exit" - ) - return parser - - -def create_options( - locustfile='locustfile', - host=None, - locust_classes=[], - port=8089, - web_host='', - no_reset_stats=None, - reset_stats=False, - no_web=False, - run_time=None, - num_clients=1, - hatch_rate=1, - master=False, - expect_slaves=1, - master_bind_host='*', - master_bind_port=5557, - slave=False, - master_host='127.0.0.1', - master_port=5557, - csvfilebase=None, - print_stats=False, - only_summary=False, - show_task_ratio=False, - show_task_ratio_json=False, - logfile=None, - loglevel='INFO', - show_version=False, - list_commands=False): - """Create options objects for passing to `run_locust` when running Locust programmatically. - - Keyword Arguments: - locustfile (str): Python module file to import, e.g. '../other.py'. (default 'locustfile') - host (str): Host to load test in the following format: http://10.21.32.33 (default: None) - locust_classes (list): Locust class callables if not importing from file (default: []) - port (int): Port on which to run web host (default: 8089) - web_host (str): Host to bind the web interface to. (default: '' (all interfaces)) - reset_stats (bool): Reset statistics once hatching has been completed. Should be set on both master and slaves when running in distributed mode (default: False) - no_web (bool): Disable the web interface, and instead start running the test immediately. Requires num_clients and hatch_rate to be specified. (default: False) - run_time (str): Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with:no-web (default: None) - num_clients (int): Number of concurrent Locust users. Only used together with no_web (default: 1) - hatch_rate (int): The rate per second in which clients are spawned. Only used together with:no-web (default: 1) - master (bool): Set locust to run in distributed mode with this process as master (default: False) - expect_slaves (int): How many slaves master should expect to connect before starting the test (only when no_web used). (default: 1) - master_bind_host (str): Interfaces (hostname, ip) that locust master should bind to. Only used when running with master. Defaults all available interfaces. (default: '*') - master_bind_port (int): Port that locust master should bind to. Only used when running with:master. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558. (default: 5557) - slave (bool): Set locust to run in distributed mode with this process as slave (default: False) - master_host (str): Host or IP address of locust master for distributed load testing. Only used when running with slave. (default: '127.0.0.1') - master_port (int): The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Note that slaves will also connect to the master node on this port + 1. (default: 5557) - csvfilebase (str): Store current request stats to files in CSV format. (default: None) - print_stats (bool): Print stats in the console (default: False) - only_summary (bool): Only print the summary stats (default: False) - show_task_ratio (bool): print table of the locust classes' task execution ratio (default: False) - show_task_ratio_json (bool): print json data of the locust classes' task execution ratio (default: False) - logfile (str): Path to log file. If not set, log will go to stdout/stderr (default: None) - loglevel (str): Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL. (default: 'INFO') - show_version (bool): show program's version number and exit (default: False) - list_commands (bool): Return list of possible locust classes and exit (default: False) - - """ - - - opts,_ = create_parser().parse_args([]) - opts.locustfile = locustfile - opts.host=host - opts.locust_classes=locust_classes # Locust class {name:callables, ...} if not loading from file path - - # web interface - opts.port=port - opts.web_host=web_host - - # No web - opts.no_web=no_web - opts.run_time=run_time - opts.num_clients=num_clients - opts.hatch_rate=hatch_rate - - # Distributed settings - # Master settings - opts.master=master - opts.expect_slaves=expect_slaves - opts.master_bind_host=master_bind_host - opts.master_bind_port=master_bind_port - # Slave settings - opts.slave=slave - opts.master_host=master_host - opts.master_port=master_port - - # Output settings - opts.reset_stats=reset_stats - opts.csvfilebase=csvfilebase - opts.print_stats=print_stats - opts.only_summary=only_summary - opts.show_task_ratio=show_task_ratio - opts.show_task_ratio_json=show_task_ratio_json - opts.logfile=logfile - opts.loglevel=loglevel - - # Miscellaneous - opts.show_version=show_version - opts.list_commands=list_commands - - return opts - - -def parse_options(args=sys.argv): - """ - Handle command-line options with optparse.OptionParser. - - Return list of arguments, largely for use in `parse_arguments`. - """ - parser = create_parser() - # Return three-tuple of parser + the output from parse_args (opt obj, args) - opts, args = parser.parse_args(args) - return parser, opts, args \ No newline at end of file From 85420c5202661e933bef3dda97e5234c205c5032 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Tue, 29 May 2018 14:30:08 +0100 Subject: [PATCH 09/12] Fixing : instead of -- typo --- locust/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locust/main.py b/locust/main.py index 5c207cbd5f..cacc10c3ab 100644 --- a/locust/main.py +++ b/locust/main.py @@ -97,7 +97,7 @@ def create_parser(): type='str', dest='master_host', 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." + 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( @@ -106,7 +106,7 @@ def create_parser(): type='int', dest='master_port', default=5557, - help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with:slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." + help="The port to connect to that is used by the locust master for distributed load testing. Only used when running with --slave. Defaults to 5557. Note that slaves will also connect to the master node on this port + 1." ) parser.add_option( @@ -115,7 +115,7 @@ def create_parser(): type='str', dest='master_bind_host', default="*", - help="Interfaces (hostname, ip) that locust master should bind to. Only used when running with:master. Defaults to * (all available interfaces)." + help="Interfaces (hostname, ip) that locust master should bind to. Only used when running with --master. Defaults to * (all available interfaces)." ) parser.add_option( @@ -124,7 +124,7 @@ def create_parser(): type='int', dest='master_bind_port', default=5557, - help="Port that locust master should bind to. Only used when running with:master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." + help="Port that locust master should bind to. Only used when running with --master. Defaults to 5557. Note that Locust will also use this port + 1, so by default the master node will bind to 5557 and 5558." ) parser.add_option( @@ -253,7 +253,7 @@ def create_parser(): help="print json data of the locust classes' task execution ratio" ) - # Version number (optparse gives you:version but we have to do it + # Version number (optparse gives you --version but we have to do it # ourselves to get -V too. sigh) parser.add_option( '-V', '--version', From a66b7e14f39312cfdf69c5f2763eda391d0a3db5 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Tue, 29 May 2018 14:41:27 +0100 Subject: [PATCH 10/12] import core before main --- locust/test/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 677f111081..7a17868f6e 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -1,5 +1,5 @@ -from locust import main from locust.core import HttpLocust, Locust, TaskSet +from locust import main from .testcases import LocustTestCase From 9e920b77035bec97f6557bf49f696b77f07365b1 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 4 Jun 2018 12:30:30 +0100 Subject: [PATCH 11/12] Remove refactoring and is_taskset/load_taskset functions --- locust/main.py | 56 ++++++++++---------------------------------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/locust/main.py b/locust/main.py index cacc10c3ab..becb5f106f 100644 --- a/locust/main.py +++ b/locust/main.py @@ -426,7 +426,7 @@ def find_locustfile(locustfile): # Implicit 'return None' if nothing was found -def is_locust(tup, ignore_prefix='_'): +def is_locust(tup): """ Takes (name, object) tuple, returns True if it's a public Locust subclass. """ @@ -436,32 +436,20 @@ def is_locust(tup, ignore_prefix='_'): and issubclass(item, Locust) and hasattr(item, "task_set") and getattr(item, "task_set") - and not name.startswith(ignore_prefix) + and not name.startswith('_') ) -def is_taskset(tup, ignore_prefix='_'): - """Takes (name, object) tuple, returns True if it's a public TaskSet subclass.""" - name, item = tup - return bool( - inspect.isclass(item) - and issubclass(item, TaskSet) - and hasattr(item, "tasks") - and not name.startswith(ignore_prefix) - and name != 'TaskSet' - ) - - -def load_filterfile(path, filter_function): +def load_locustfile(path): """ - Import given path and return (docstring, callables) of all clases that pass the test. + Import given locustfile path and return (docstring, callables). - Specifically, the classfile's ``__doc__`` attribute (a string) and a + Specifically, the locustfile's ``__doc__`` attribute (a string) and a dictionary of ``{'name': callable}`` containing all callables which pass - the `filter_function` test. + the "is a Locust" test. """ # Get directory and locustfile name - directory, filterfile = os.path.split(path) + directory, locustfile = os.path.split(path) # If the directory isn't in the PYTHONPATH, add it so our import will work added_to_path = False index = None @@ -480,7 +468,7 @@ def load_filterfile(path, filter_function): sys.path.insert(0, directory) del sys.path[i + 1] # Perform the import (trimming off the .py) - imported = __import__(os.path.splitext(filterfile)[0]) + imported = __import__(os.path.splitext(locustfile)[0]) # Remove directory from path if we added it ourselves (just to be neat) if added_to_path: del sys.path[0] @@ -489,32 +477,9 @@ def load_filterfile(path, filter_function): sys.path.insert(index + 1, directory) del sys.path[0] # Return our two-tuple - classes = dict(filter(filter_function, vars(imported).items())) - return imported.__doc__, classes - + locusts = dict(filter(is_locust, vars(imported).items())) + return imported.__doc__, locusts -def load_locustfile(path): - """ - Import given locustfile path and return (docstring, callables). - - Specifically, the locustfile's ``__doc__`` attribute (a string) and a - dictionary of ``{'name': callable}`` containing all callables which pass - the "is a Locust" test. - """ - return load_filterfile(path, is_locust) - - -def load_tasksetfile(path): - """ - Import given tasksetfile path and return (docstring, callables). - - Specifically, the tasksetfile's ``__doc__`` attribute (a string) and a - dictionary of ``{'name': callable}`` containing all callables which pass - the "is a TaskSet" test (`is_taskset`). - """ - return load_filterfile(path, is_taskset) - -default_options = create_options() def run_locust(options, arguments=[], cli_mode=False): """Run Locust programmatically. @@ -706,5 +671,6 @@ def main(): args = arguments[1:] run_locust(options=options, arguments=args, cli_mode=True) + if __name__ == '__main__': main() From 28f3a82ae398a45edde215284093a26cc339b209 Mon Sep 17 00:00:00 2001 From: "g.punter" Date: Mon, 4 Jun 2018 12:32:27 +0100 Subject: [PATCH 12/12] Remove import TaskSet --- locust/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index becb5f106f..2dc704e809 100644 --- a/locust/main.py +++ b/locust/main.py @@ -12,7 +12,7 @@ import locust from . import events, runners, web -from .core import HttpLocust, Locust, TaskSet +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