change: make locusts as hrun sub-command, usage: hrun locusts -h

This commit is contained in:
debugtalk
2020-04-12 23:29:00 +08:00
parent 8a39bbb06a
commit b9392d7cce
10 changed files with 211 additions and 181 deletions

View File

@@ -8,6 +8,7 @@
- remove compatibility with testcase/testsuite format v1
- make `startproject` as hrun sub-command, usage: `hrun startproject <project_name>`
- make `har2case` as hrun sub-command, usage: `hrun har2case -h`
- make `locusts` as hrun sub-command, usage: `hrun locusts -h`
## 3.0.1 (2020-03-24)

View File

@@ -4,10 +4,16 @@ import sys
from loguru import logger
if len(sys.argv) >= 2 and sys.argv[1] == "locusts":
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey
monkey.patch_ssl()
from httprunner import __description__, __version__
from httprunner.api import HttpRunner
from httprunner.ext.har2case import init_har2case_parser, main_har2case
from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold
from httprunner.ext.locusts import init_parser_locusts, main_locusts
from httprunner.report import gen_html_report
@@ -85,8 +91,13 @@ def main():
sub_parser_run = init_parser_run(subparsers)
sub_parser_scaffold = init_parser_scaffold(subparsers)
sub_parser_har2case = init_har2case_parser(subparsers)
sub_parser_locusts = init_parser_locusts(subparsers)
args = parser.parse_args()
extra_args = []
if len(sys.argv) >= 2 and sys.argv[1] == "locusts":
args, extra_args = parser.parse_known_args()
else:
args = parser.parse_args()
if args.version:
print(f"{__version__}")
@@ -121,6 +132,14 @@ def main():
main_har2case(args)
elif sys.argv[1] == "locusts":
# hrun locusts
if len(sys.argv) == 2:
sub_parser_locusts.print_help()
sys.exit(0)
main_locusts(args, extra_args)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,79 @@
import multiprocessing
import sys
from loguru import logger
from httprunner import __version__
from httprunner.ext.locusts.core import start_locust_main, parse_locustfile, quick_run_locusts, start_master, \
start_slaves
CPU_COUNT = multiprocessing.cpu_count()
def init_parser_locusts(subparsers):
sub_parser_locusts = subparsers.add_parser(
"locusts", help="Run load test with locust.")
sub_parser_locusts.add_argument(
'--locust-help', action='store_true', default=False,
help="Show locust help.")
sub_parser_locusts.add_argument(
"--master", action='store_true', default=False, help="Start locust master.")
sub_parser_locusts.add_argument(
"--slaves", type=int, help="Specify locust slave number.")
sub_parser_locusts.add_argument(
"--quickstart", action='store_true', default=False,
help=f"Start locust master with {CPU_COUNT} slaves.")
return sub_parser_locusts
def main_locusts(args, extra_args):
""" Performance test with locust: parse command line options and run commands.
"""
logger.info(f"HttpRunner version: {__version__}")
sys.argv = ["locust", *extra_args]
if args.locust_help:
sys.argv = ["locust", "-h"]
start_locust_main()
def get_arg_index(*target_args):
for arg in target_args:
if arg not in sys.argv:
continue
return sys.argv.index(arg) + 1
return None
# set logging level
loglevel_index = get_arg_index("-L", "--loglevel")
if loglevel_index and loglevel_index < len(sys.argv):
loglevel = sys.argv[loglevel_index]
loglevel = loglevel.upper()
else:
# default
loglevel = "INFO"
logger.remove()
logger.add(sys.stdout, level=loglevel)
# convert httprunner yaml/json case to locustfile.py
try:
testcase_index = get_arg_index("-f", "--locustfile")
assert testcase_index and testcase_index < len(sys.argv)
testcase_file_path = sys.argv[testcase_index]
sys.argv[testcase_index] = parse_locustfile(testcase_file_path)
except AssertionError:
print("Testcase file is not specified, exit.")
sys.exit(1)
manager = multiprocessing.Manager()
try:
if args.quickstart:
quick_run_locusts(CPU_COUNT)
elif args.master:
start_master(sys.argv)
elif args.slaves:
start_slaves(args.slaves)
except KeyboardInterrupt:
manager.shutdown()

View File

@@ -1,4 +0,0 @@
from httprunner.ext.locusts.cli import main
if __name__ == "__main__":
main()

View File

@@ -1,174 +0,0 @@
try:
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey
monkey.patch_ssl()
from locust import main as locust_main
except ImportError:
msg = """
Locust is not installed, install first and try again.
install with pip:
$ pip install locustio
"""
print(msg)
import sys
sys.exit(0)
import io
import multiprocessing
import os
import sys
from loguru import logger
from httprunner import __version__
def parse_locustfile(file_path):
""" parse testcase file and return locustfile path.
if file_path is a Python file, assume it is a locustfile
if file_path is a YAML/JSON file, convert it to locustfile
"""
if not os.path.isfile(file_path):
logger.error("file path invalid, exit.")
sys.exit(1)
file_suffix = os.path.splitext(file_path)[1]
if file_suffix == ".py":
locustfile_path = file_path
elif file_suffix in ['.yaml', '.yml', '.json']:
locustfile_path = gen_locustfile(file_path)
else:
# '' or other suffix
logger.error("file type should be YAML/JSON/Python, exit.")
sys.exit(1)
return locustfile_path
def gen_locustfile(testcase_file_path):
""" generate locustfile from template.
"""
locustfile_path = 'locustfile.py'
template_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"locustfile_template.py"
)
with io.open(template_path, encoding='utf-8') as template:
with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile:
template_content = template.read()
template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path)
locustfile.write(template_content)
return locustfile_path
def start_locust_main():
locust_main.main()
def start_master(sys_argv):
sys_argv.append("--master")
sys.argv = sys_argv
start_locust_main()
def start_slave(sys_argv):
if "--slave" not in sys_argv:
sys_argv.extend(["--slave"])
sys.argv = sys_argv
start_locust_main()
def run_locusts_with_processes(sys_argv, processes_count):
processes = []
manager = multiprocessing.Manager()
for _ in range(processes_count):
p_slave = multiprocessing.Process(target=start_slave, args=(sys_argv,))
p_slave.daemon = True
p_slave.start()
processes.append(p_slave)
try:
if "--slave" in sys_argv:
[process.join() for process in processes]
else:
start_master(sys_argv)
except KeyboardInterrupt:
manager.shutdown()
def main():
""" Performance test with locust: parse command line options and run commands.
"""
print(f"HttpRunner version: {__version__}")
sys.argv[0] = 'locust'
if len(sys.argv) == 1:
sys.argv.extend(["-h"])
if sys.argv[1] in ["-h", "--help", "-V", "--version"]:
start_locust_main()
def get_arg_index(*target_args):
for arg in target_args:
if arg not in sys.argv:
continue
return sys.argv.index(arg) + 1
return None
# set logging level
loglevel_index = get_arg_index("-L", "--loglevel")
if loglevel_index and loglevel_index < len(sys.argv):
loglevel = sys.argv[loglevel_index]
loglevel = loglevel.upper()
else:
# default
loglevel = "WARNING"
logger.remove()
logger.add(sys.stdout, level=loglevel)
# get testcase file path
try:
testcase_index = get_arg_index("-f", "--locustfile")
assert testcase_index and testcase_index < len(sys.argv)
except AssertionError:
print("Testcase file is not specified, exit.")
sys.exit(1)
testcase_file_path = sys.argv[testcase_index]
sys.argv[testcase_index] = parse_locustfile(testcase_file_path)
if "--processes" in sys.argv:
""" locusts -f locustfile.py --processes 4
"""
if "--no-web" in sys.argv:
logger.error("conflict parameter args: --processes & --no-web. \nexit.")
sys.exit(1)
processes_index = sys.argv.index('--processes')
processes_count_index = processes_index + 1
if processes_count_index >= len(sys.argv):
""" do not specify processes count explicitly
locusts -f locustfile.py --processes
"""
processes_count = multiprocessing.cpu_count()
logger.warning(f"processes count not specified, use {processes_count} by default.")
else:
try:
""" locusts -f locustfile.py --processes 4 """
processes_count = int(sys.argv[processes_count_index])
sys.argv.pop(processes_count_index)
except ValueError:
""" locusts -f locustfile.py --processes -P 8888 """
processes_count = multiprocessing.cpu_count()
logger.warning(f"processes count not specified, use {processes_count} by default.")
sys.argv.pop(processes_index)
run_locusts_with_processes(sys.argv, processes_count)
else:
start_locust_main()

View File

@@ -0,0 +1,110 @@
import io
import multiprocessing
import os
import sys
from loguru import logger
try:
from locust import main as locust_main
except ImportError:
msg = """
Locust is not installed, install first and try again.
install with pip:
$ pip install locustio
"""
logger.error(msg)
sys.exit(0)
def parse_locustfile(file_path):
""" parse testcase file and return locustfile path.
if file_path is a Python file, assume it is a locustfile
if file_path is a YAML/JSON file, convert it to locustfile
"""
if not os.path.isfile(file_path):
logger.error("file path invalid, exit.")
sys.exit(1)
file_suffix = os.path.splitext(file_path)[1]
if file_suffix == ".py":
locustfile_path = file_path
elif file_suffix in ['.yaml', '.yml', '.json']:
locustfile_path = gen_locustfile(file_path)
else:
# '' or other suffix
logger.error("file type should be YAML/JSON/Python, exit.")
sys.exit(1)
return locustfile_path
def gen_locustfile(testcase_file_path):
""" generate locustfile from template.
"""
locustfile_path = 'locustfile.py'
template_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"locustfile_template.py"
)
with io.open(template_path, encoding='utf-8') as template:
with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile:
template_content = template.read()
template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path)
locustfile.write(template_content)
return locustfile_path
def start_locust_main():
logger.info(f"run command: {sys.argv}")
locust_main.main()
def start_master(sys_argv):
sys_argv.append("--master")
sys.argv = sys_argv
start_locust_main()
def start_slave(sys_argv):
if "--slave" not in sys_argv:
sys_argv.extend(["--slave"])
sys.argv = sys_argv
start_locust_main()
def init_slave_processes(slave_num):
""" init specified number of locust slave processes."""
processes = []
for _ in range(slave_num):
p_slave = multiprocessing.Process(target=start_slave, args=(sys.argv,))
p_slave.daemon = True
p_slave.start()
processes.append(p_slave)
return processes
def start_slaves(slave_num):
logger.info(f"Start {slave_num} locust slaves ...")
processes = init_slave_processes(slave_num)
[process.join() for process in processes]
def quick_run_locusts(slave_num):
""" quick start locust master and multiple slaves.
Args:
slave_num: locust slaves number
"""
logger.info(f"Start locust master with {slave_num} slaves ...")
processes = init_slave_processes(slave_num)
processes.append(
multiprocessing.Process(target=start_master, args=(sys.argv,))
)
[process.join() for process in processes]

View File

@@ -8,7 +8,7 @@ class TestLocust(unittest.TestCase):
def test_prepare_locust_tests(self):
path = os.path.join(
os.getcwd(), 'tests/locust_tests/demo_locusts.yml')
os.path.dirname(__file__), "data", "demo_locusts.yml")
locust_tests = prepare_locust_tests(path)
self.assertEqual(len(locust_tests), 2 + 3)
name_list = [

View File

@@ -48,7 +48,6 @@ fastapi = "^0.49.0"
hrun = "httprunner.cli:main"
ate = "httprunner.cli:main"
httprunner = "httprunner.cli:main"
locusts = "httprunner.ext.locusts.cli:main"
[build-system]
requires = ["poetry>=1.0.0"]