Merge pull request #733 from httprunner/dev

2.3.0
This commit is contained in:
debugtalk
2019-10-26 22:35:48 +08:00
committed by GitHub
41 changed files with 750 additions and 358 deletions

2
.gitignore vendored
View File

@@ -13,5 +13,5 @@ logs
.coverage .coverage
locustfile.py locustfile.py
site/ site/
poetry.lock
reports reports
.venv

View File

@@ -5,10 +5,11 @@ python:
- 3.5 - 3.5
- 3.6 - 3.6
matrix: matrix:
include: include: # Required for Python 3.7+
- python: 3.7 - python: 3.7
dist: xenial # Required for Python 3.7 dist: xenial
sudo: true # Required for Python 3.7 - python: 3.8
dist: xenial
install: install:
- pip install poetry - pip install poetry
- poetry install -vvv - poetry install -vvv

View File

@@ -1,5 +1,24 @@
# Release History # Release History
## 2.3.0 (2019-10-27)
**Added**
- feat: implement plugin system prototype, make locusts as plugin
- test: add Python 3.8 to Travis-CI
- feat: add `__main__.py`, `python -m httprunner` can be used to hrun tests
**Changed**
- update dependency versions in pyproject.toml
- rename folder, httprunner/templates => httprunner/static
- log httprunner version before running tests
- remove unused import & code
**Fixed**
- fix #707: duration stat error in multiple testsuites
## 2.2.6 (2019-09-18) ## 2.2.6 (2019-09-18)
**Added** **Added**

View File

@@ -47,7 +47,7 @@ HttpRunner is rich documented.
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。 关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
![](httprunner/templates/qrcode.jpg) ![](httprunner/static/qrcode.jpg)
[Requests]: http://docs.python-requests.org/en/master/ [Requests]: http://docs.python-requests.org/en/master/
[unittest]: https://docs.python.org/3/library/unittest.html [unittest]: https://docs.python.org/3/library/unittest.html

View File

@@ -1,4 +1,4 @@
__version__ = "2.2.6" __version__ = "2.3.0"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
__all__ = ["__version__", "__description__"] __all__ = ["__version__", "__description__"]

6
httprunner/__main__.py Normal file
View File

@@ -0,0 +1,6 @@
import sys
from httprunner.cli import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,6 +1,3 @@
# encoding: utf-8
import os
import unittest import unittest
from httprunner import (__version__, exceptions, loader, logger, parser, from httprunner import (__version__, exceptions, loader, logger, parser,
@@ -23,7 +20,6 @@ class HttpRunner(object):
""" """
logger.setup_logger(log_level, log_file) logger.setup_logger(log_level, log_file)
logger.log_info("HttpRunner version: {}".format(__version__))
self.exception_stage = "initialize HttpRunner()" self.exception_stage = "initialize HttpRunner()"
kwargs = { kwargs = {
@@ -274,6 +270,7 @@ class HttpRunner(object):
dict: valid testcase/testsuite data dict: valid testcase/testsuite data
""" """
logger.log_info("HttpRunner version: {}".format(__version__))
if validator.is_testcase_path(path_or_tests): if validator.is_testcase_path(path_or_tests):
return self.run_path(path_or_tests, dot_env_path, mapping) return self.run_path(path_or_tests, dot_env_path, mapping)
elif validator.is_testcases(path_or_tests): elif validator.is_testcases(path_or_tests):
@@ -286,31 +283,3 @@ class HttpRunner(object):
""" get test reuslt summary. """ get test reuslt summary.
""" """
return self._summary return self._summary
def prepare_locust_tests(path):
""" prepare locust testcases
Args:
path (str): testcase file path.
Returns:
list: locust tests data
[
testcase1_dict,
testcase2_dict
]
"""
tests_mapping = loader.load_tests(path)
testcases = parser.parse_tests(tests_mapping)
locust_tests = []
for testcase in testcases:
testcase_weight = testcase.get("config", {}).pop("weight", 1)
for _ in range(testcase_weight):
locust_tests.append(testcase)
return locust_tests

View File

@@ -5,7 +5,6 @@ Built-in dependent functions used in YAML/JSON testcases.
""" """
import datetime import datetime
import json
import os import os
import random import random
import re import re
@@ -13,10 +12,11 @@ import string
import time import time
import filetype import filetype
from httprunner.compat import basestring, builtin_str, integer_types, str
from httprunner.exceptions import ParamsError
from requests_toolbelt import MultipartEncoder from requests_toolbelt import MultipartEncoder
from httprunner.compat import basestring, builtin_str, integer_types
from httprunner.exceptions import ParamsError
PWD = os.getcwd() PWD = os.getcwd()

View File

@@ -1,18 +1,20 @@
# encoding: utf-8 import argparse
import sys
from httprunner import __description__, __version__
from httprunner.api import HttpRunner
from httprunner.compat import is_py2
from httprunner.logger import color_print
from httprunner.utils import (create_scaffold, get_python2_retire_msg,
prettify_json_file)
from httprunner.validator import validate_json_file
def main_hrun(): def main():
""" API test: parse command line options and run commands. """ API test: parse command line options and run commands.
""" """
import sys if is_py2:
import argparse color_print(get_python2_retire_msg(), "YELLOW")
from httprunner.logger import color_print
from httprunner import __description__, __version__
from httprunner.api import HttpRunner
from httprunner.compat import is_py2
from httprunner.validator import validate_json_file
from httprunner.utils import (create_scaffold, get_python2_retire_msg,
prettify_json_file)
parser = argparse.ArgumentParser(description=__description__) parser = argparse.ArgumentParser(description=__description__)
parser.add_argument( parser.add_argument(
@@ -57,24 +59,26 @@ def main_hrun():
args = parser.parse_args() args = parser.parse_args()
if is_py2: if len(sys.argv) == 1:
color_print(get_python2_retire_msg(), "YELLOW") # no argument passed
parser.print_help()
return 0
if args.version: if args.version:
color_print("{}".format(__version__), "GREEN") color_print("{}".format(__version__), "GREEN")
exit(0) return 0
if args.validate: if args.validate:
validate_json_file(args.validate) validate_json_file(args.validate)
exit(0) return 0
if args.prettify: if args.prettify:
prettify_json_file(args.prettify) prettify_json_file(args.prettify)
exit(0) return 0
project_name = args.startproject project_name = args.startproject
if project_name: if project_name:
create_scaffold(project_name) create_scaffold(project_name)
exit(0) return 0
runner = HttpRunner( runner = HttpRunner(
failfast=args.failfast, failfast=args.failfast,
@@ -85,6 +89,7 @@ def main_hrun():
log_file=args.log_file, log_file=args.log_file,
report_file=args.report_file report_file=args.report_file
) )
try: try:
for path in args.testcase_paths: for path in args.testcase_paths:
runner.run(path, dot_env_path=args.dot_env_path) runner.run(path, dot_env_path=args.dot_env_path)
@@ -93,123 +98,10 @@ def main_hrun():
raise raise
if runner.summary and runner.summary["success"]: if runner.summary and runner.summary["success"]:
sys.exit(0) return 0
else: else:
sys.exit(1) return 1
def main_locust(): if __name__ == '__main__':
""" Performance test with locust: parse command line options and run commands. sys.exit(main())
"""
try:
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey; monkey.patch_ssl()
import multiprocessing
import sys
from httprunner import logger
from httprunner import locusts
except ImportError:
msg = "Locust is not installed, install first and try again.\n"
msg += "install command: pip install locustio"
print(msg)
exit(1)
sys.argv[0] = 'locust'
if len(sys.argv) == 1:
sys.argv.extend(["-h"])
if sys.argv[1] in ["-h", "--help", "-V", "--version"]:
locusts.start_locust_main()
sys.exit(0)
# set logging level
if "-L" in sys.argv:
loglevel_index = sys.argv.index('-L') + 1
elif "--loglevel" in sys.argv:
loglevel_index = sys.argv.index('--loglevel') + 1
else:
loglevel_index = None
if loglevel_index and loglevel_index < len(sys.argv):
loglevel = sys.argv[loglevel_index]
else:
# default
loglevel = "WARNING"
logger.setup_logger(loglevel)
# get testcase file path
try:
if "-f" in sys.argv:
testcase_index = sys.argv.index('-f') + 1
elif "--locustfile" in sys.argv:
testcase_index = sys.argv.index('--locustfile') + 1
else:
testcase_index = None
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] = locusts.parse_locustfile(testcase_file_path)
if "--processes" in sys.argv:
""" locusts -f locustfile.py --processes 4
"""
if "--no-web" in sys.argv:
logger.log_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.log_warning("processes count not specified, use {} by default.".format(processes_count))
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.log_warning("processes count not specified, use {} by default.".format(processes_count))
sys.argv.pop(processes_index)
locusts.run_locusts_with_processes(sys.argv, processes_count)
else:
locusts.start_locust_main()
if __name__ == "__main__":
""" debugging mode
"""
import sys
import os
if len(sys.argv) == 0:
exit(0)
sys.path.insert(0, os.getcwd())
cmd = sys.argv.pop(1)
if cmd in ["hrun", "httprunner", "ate"]:
main_hrun()
elif cmd in ["locust", "locusts"]:
main_locust()
else:
from httprunner.logger import color_print
color_print("Miss debugging type.", "RED")
example = "\n".join([
"e.g.",
"python -m httprunner.cli hrun /path/to/testcase_file",
"python -m httprunner.cli locusts -f /path/to/testcase_file"
])
color_print(example, "yellow")

View File

@@ -4,12 +4,13 @@ import time
import requests import requests
import urllib3 import urllib3
from httprunner import logger
from httprunner.utils import lower_dict_keys, omit_long_data
from requests import Request, Response from requests import Request, Response
from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema, from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
RequestException) RequestException)
from httprunner import logger
from httprunner.utils import lower_dict_keys, omit_long_data
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -30,8 +31,8 @@ class HttpSession(requests.Session):
This is a slightly extended version of `python-request <http://python-requests.org>`_'s This is a slightly extended version of `python-request <http://python-requests.org>`_'s
:py:class:`requests.Session` class and mostly this class works exactly the same. :py:class:`requests.Session` class and mostly this class works exactly the same.
""" """
def __init__(self, *args, **kwargs): def __init__(self):
super(HttpSession, self).__init__(*args, **kwargs) super(HttpSession, self).__init__()
self.init_meta_data() self.init_meta_data()
def init_meta_data(self): def init_meta_data(self):

View File

@@ -7,7 +7,6 @@ httprunner.compat
This module handles import compatibility issues between Python 2 and This module handles import compatibility issues between Python 2 and
Python 3. Python 3.
""" """
from collections import OrderedDict
try: try:
import simplejson as json import simplejson as json

View File

@@ -1,5 +1,3 @@
import collections
import copy
import csv import csv
import importlib import importlib
import io import io
@@ -8,7 +6,8 @@ import os
import sys import sys
import yaml import yaml
from httprunner import built_in, exceptions, logger, parser, utils, validator
from httprunner import built_in, exceptions, logger, utils, validator
try: try:
# PyYAML version >= 5.1 # PyYAML version >= 5.1

View File

@@ -1,88 +0,0 @@
# encoding: utf-8
import io
import multiprocessing
import os
import sys
from httprunner.logger import color_print
from httprunner import loader
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):
color_print("file path invalid, exit.", "RED")
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
color_print("file type should be YAML/JSON/Python, exit.", "RED")
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__)),
"templates",
"locustfile_template"
)
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():
from locust.main import 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()

View File

@@ -1,6 +1,5 @@
# encoding: utf-8
import logging import logging
import os
import sys import sys
from colorama import Fore, init from colorama import Fore, init
@@ -8,6 +7,9 @@ from colorlog import ColoredFormatter
init(autoreset=True) init(autoreset=True)
LOG_LEVEL = "INFO"
LOG_FILE_PATH = ""
log_colors_config = { log_colors_config = {
'DEBUG': 'cyan', 'DEBUG': 'cyan',
'INFO': 'green', 'INFO': 'green',
@@ -15,11 +17,28 @@ log_colors_config = {
'ERROR': 'red', 'ERROR': 'red',
'CRITICAL': 'red', 'CRITICAL': 'red',
} }
logger = logging.getLogger("httprunner") loggers = {}
def setup_logger(log_level, log_file=None): def setup_logger(log_level, log_file=None):
global LOG_LEVEL
LOG_LEVEL = log_level
if log_file:
global LOG_FILE_PATH
LOG_FILE_PATH = log_file
def get_logger(name=None):
"""setup logger with ColoredFormatter.""" """setup logger with ColoredFormatter."""
name = name or "httprunner"
logger_key = "".join([name, LOG_LEVEL, LOG_FILE_PATH])
if logger_key in loggers:
return loggers[logger_key]
_logger = logging.getLogger(name)
log_level = LOG_LEVEL
level = getattr(logging, log_level.upper(), None) level = getattr(logging, log_level.upper(), None)
if not level: if not level:
color_print("Invalid log level: %s" % log_level, "RED") color_print("Invalid log level: %s" % log_level, "RED")
@@ -29,21 +48,26 @@ def setup_logger(log_level, log_file=None):
if level >= logging.INFO: if level >= logging.INFO:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
_logger.setLevel(level)
if LOG_FILE_PATH:
log_dir = os.path.dirname(LOG_FILE_PATH)
if not os.path.isdir(log_dir):
os.makedirs(log_dir)
handler = logging.FileHandler(LOG_FILE_PATH, encoding="utf-8")
else:
handler = logging.StreamHandler(sys.stdout)
formatter = ColoredFormatter( formatter = ColoredFormatter(
u"%(log_color)s%(bg_white)s%(levelname)-8s%(reset)s %(message)s", u"%(log_color)s%(bg_white)s%(levelname)-8s%(reset)s %(message)s",
datefmt=None, datefmt=None,
reset=True, reset=True,
log_colors=log_colors_config log_colors=log_colors_config
) )
if log_file:
handler = logging.FileHandler(log_file, encoding="utf-8")
else:
handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) _logger.addHandler(handler)
logger.setLevel(level)
loggers[logger_key] = _logger
return _logger
def coloring(text, color="WHITE"): def coloring(text, color="WHITE"):
@@ -61,7 +85,8 @@ def log_with_color(level):
""" """
def wrapper(text): def wrapper(text):
color = log_colors_config[level.upper()] color = log_colors_config[level.upper()]
getattr(logger, level.lower())(coloring(text, color)) _logger = get_logger()
getattr(_logger, level.lower())(coloring(text, color))
return wrapper return wrapper

View File

@@ -0,0 +1,2 @@
# NOTICE:
# This file should not be deleted, or ImportError will be raised in Python 2.7 when importing plugin

View File

@@ -0,0 +1,104 @@
# locusts
## Installation
```shell script
$ pip install locustio
```
## Usage
```shell script
$ locusts -f xxx.yml
```
```shell script
$ locusts -f xxx.yml --processes
```
```shell script
$ python3 -m httprunner.plugins.locusts -h
Usage: locust [options] [LocustClass [LocustClass2 ... ]]
Options:
-h, --help show this help message and exit
-H HOST, --host=HOST Host to load test in the following format:
http://10.21.32.33
--web-host=WEB_HOST Host to bind the web interface to. Defaults to '' (all
interfaces)
-P PORT, --port=PORT, --web-port=PORT
Port on which to run web host
-f LOCUSTFILE, --locustfile=LOCUSTFILE
Python module file to import, e.g. '../other.py'.
Default: locustfile
--csv=CSVFILEBASE, --csv-base-name=CSVFILEBASE
Store current request stats to files in CSV format.
--master Set locust to run in distributed mode with this
process as master
--slave Set locust to run in distributed mode with this
process as slave
--master-host=MASTER_HOST
Host or IP address of locust master for distributed
load testing. Only used when running with --slave.
Defaults to 127.0.0.1.
--master-port=MASTER_PORT
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.
--master-bind-host=MASTER_BIND_HOST
Interfaces (hostname, ip) that locust master should
bind to. Only used when running with --master.
Defaults to * (all available interfaces).
--master-bind-port=MASTER_BIND_PORT
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.
--heartbeat-liveness=HEARTBEAT_LIVENESS
set number of seconds before failed heartbeat from
slave
--heartbeat-interval=HEARTBEAT_INTERVAL
set number of seconds delay between slave heartbeats
to master
--expect-slaves=EXPECT_SLAVES
How many slaves master should expect to connect before
starting the test (only when --no-web used).
--no-web Disable the web interface, and instead start running
the test immediately. Requires -c and -r to be
specified.
-c NUM_CLIENTS, --clients=NUM_CLIENTS
Number of concurrent Locust users. Only used together
with --no-web
-r HATCH_RATE, --hatch-rate=HATCH_RATE
The rate per second in which clients are spawned. Only
used together with --no-web
-t RUN_TIME, --run-time=RUN_TIME
Stop after the specified amount of time, e.g. (300s,
20m, 3h, 1h30m, etc.). Only used together with --no-
web
-L LOGLEVEL, --loglevel=LOGLEVEL
Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL.
Default is INFO.
--logfile=LOGFILE Path to log file. If not set, log will go to
stdout/stderr
--print-stats Print stats in the console
--only-summary Only print the summary stats
--no-reset-stats [DEPRECATED] Do not reset statistics once hatching has
been completed. This is now the default behavior. See
--reset-stats to disable
--reset-stats Reset statistics once hatching has been completed.
Should be set on both master and slaves when running
in distributed mode
-l, --list Show list of possible locust classes and exit
--show-task-ratio print table of the locust classes' task execution
ratio
--show-task-ratio-json
print json data of the locust classes' task execution
ratio
-V, --version show program's version number and exit
--exit-code-on-error=EXIT_CODE_ON_ERROR
sets the exit code to post on error
```

View File

View File

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

View File

@@ -0,0 +1,169 @@
try:
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey
monkey.patch_ssl()
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 httprunner import logger
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.color_print("file path invalid, exit.", "RED")
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.color_print("file type should be YAML/JSON/Python, exit.", "RED")
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():
from locust.main import 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.
"""
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]
else:
# default
loglevel = "WARNING"
logger.setup_logger(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.log_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.log_warning("processes count not specified, use {} by default.".format(processes_count))
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.log_warning("processes count not specified, use {} by default.".format(processes_count))
sys.argv.pop(processes_index)
run_locusts_with_processes(sys.argv, processes_count)
else:
start_locust_main()

View File

@@ -1,13 +1,13 @@
import logging import logging
import random import random
import zmq
from httprunner.exceptions import MyBaseError, MyBaseFailure
from httprunner.api import prepare_locust_tests
from httprunner.runner import Runner
from locust import HttpLocust, TaskSet, task from locust import HttpLocust, TaskSet, task
from locust.events import request_failure from locust.events import request_failure
from httprunner.exceptions import MyBaseError, MyBaseFailure
from httprunner.plugins.locusts.utils import prepare_locust_tests
from httprunner.runner import Runner
logging.getLogger().setLevel(logging.CRITICAL) logging.getLogger().setLevel(logging.CRITICAL)
logging.getLogger('locust.main').setLevel(logging.INFO) logging.getLogger('locust.main').setLevel(logging.INFO)
logging.getLogger('locust.runners').setLevel(logging.INFO) logging.getLogger('locust.runners').setLevel(logging.INFO)
@@ -38,5 +38,6 @@ class WebPageUser(HttpLocust):
min_wait = 10 min_wait = 10
max_wait = 30 max_wait = 30
# file_path is generated on locusts startup
file_path = "$TESTCASE_FILE" file_path = "$TESTCASE_FILE"
tests = prepare_locust_tests(file_path) tests = prepare_locust_tests(file_path)

View File

@@ -0,0 +1,29 @@
from httprunner import loader, parser
def prepare_locust_tests(path):
""" prepare locust testcases
Args:
path (str): testcase file path.
Returns:
list: locust tests data
[
testcase1_dict,
testcase2_dict
]
"""
tests_mapping = loader.load_tests(path)
testcases = parser.parse_tests(tests_mapping)
locust_tests = []
for testcase in testcases:
testcase_weight = testcase.get("config", {}).pop("weight", 1)
for _ in range(testcase_weight):
locust_tests.append(testcase)
return locust_tests

View File

@@ -1,5 +1,3 @@
# encoding: utf-8
import io import io
import os import os
import platform import platform
@@ -9,10 +7,11 @@ from base64 import b64encode
from collections import Iterable from collections import Iterable
from datetime import datetime from datetime import datetime
import requests
from httprunner import __version__, loader, logger
from httprunner.compat import basestring, bytes, json, numeric_types
from jinja2 import Template, escape from jinja2 import Template, escape
from requests.cookies import RequestsCookieJar
from httprunner import __version__, logger
from httprunner.compat import basestring, bytes, json, numeric_types
def get_platform(): def get_platform():
@@ -82,9 +81,14 @@ def aggregate_stat(origin_stat, new_stat):
if key not in origin_stat: if key not in origin_stat:
origin_stat[key] = new_stat[key] origin_stat[key] = new_stat[key]
elif key == "start_at": elif key == "start_at":
# start datetime , duration=current_time - min(stat_at) # start datetime
origin_stat[key] = min(origin_stat[key], new_stat[key]) origin_stat["start_at"] = min(origin_stat["start_at"], new_stat["start_at"])
origin_stat["duration"] = time.time() - origin_stat[key] elif key == "duration":
# duration = max_end_time - min_start_time
max_end_time = max(origin_stat["start_at"] + origin_stat["duration"],
new_stat["start_at"] + new_stat["duration"])
min_start_time = min(origin_stat["start_at"], new_stat["start_at"])
origin_stat["duration"] = max_end_time - min_start_time
else: else:
origin_stat[key] += new_stat[key] origin_stat[key] += new_stat[key]
@@ -150,7 +154,7 @@ def __stringify_request(request_data):
# class instance, e.g. MultipartEncoder() # class instance, e.g. MultipartEncoder()
value = repr(value) value = repr(value)
elif isinstance(value, requests.cookies.RequestsCookieJar): elif isinstance(value, RequestsCookieJar):
value = value.get_dict() value = value.get_dict()
request_data[key] = value request_data[key] = value
@@ -209,7 +213,7 @@ def __stringify_response(response_data):
# class instance, e.g. MultipartEncoder() # class instance, e.g. MultipartEncoder()
value = repr(value) value = repr(value)
elif isinstance(value, requests.cookies.RequestsCookieJar): elif isinstance(value, RequestsCookieJar):
value = value.get_dict() value = value.get_dict()
response_data[key] = value response_data[key] = value
@@ -283,7 +287,7 @@ def render_html_report(summary, report_template=None, report_dir=None, report_fi
if not report_template: if not report_template:
report_template = os.path.join( report_template = os.path.join(
os.path.abspath(os.path.dirname(__file__)), os.path.abspath(os.path.dirname(__file__)),
"templates", "static",
"report_template.html" "report_template.html"
) )
logger.log_debug("No html report template specified, use default.") logger.log_debug("No html report template specified, use default.")

View File

@@ -1,12 +1,10 @@
# encoding: utf-8
import json
import re import re
from collections import OrderedDict
import jsonpath import jsonpath
from httprunner import exceptions, logger, utils from httprunner import exceptions, logger, utils
from httprunner.compat import OrderedDict, basestring, is_py2 from httprunner.compat import basestring, is_py2
text_extractor_regexp_compile = re.compile(r".*\(.*\).*") text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
@@ -55,7 +53,7 @@ class ResponseObject(object):
} }
] ]
}, },
"message": "操作成功" "message": "success"
} }
:param field: Jsonpath expression, e.g. 1)$.code 2) $..items.*.id :param field: Jsonpath expression, e.g. 1)$.code 2) $..items.*.id

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -7,7 +7,6 @@ import itertools
import json import json
import os.path import os.path
import re import re
import string
from datetime import datetime from datetime import datetime
from httprunner import exceptions, logger from httprunner import exceptions, logger

View File

@@ -7,11 +7,11 @@ import types
from httprunner import exceptions, logger from httprunner import exceptions, logger
""" validate data format """ validate data format
TODO: refactor with JSON schema validate TODO: refactor with JSON schema validate
""" """
def is_testcase(data_structure): def is_testcase(data_structure):
""" check if data_structure is a testcase. """ check if data_structure is a testcase.

237
poetry.lock generated Normal file
View File

@@ -0,0 +1,237 @@
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2019.9.11"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = "*"
version = "3.0.4"
[[package]]
category = "dev"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0"
[[package]]
category = "main"
description = "Cross-platform colored terminal text."
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.4.1"
[[package]]
category = "main"
description = "Log formatting with colors!"
name = "colorlog"
optional = false
python-versions = "*"
version = "4.0.2"
[package.dependencies]
colorama = "*"
[[package]]
category = "dev"
description = "Code coverage measurement for Python"
name = "coverage"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
version = "4.5.4"
[[package]]
category = "dev"
description = "Show coverage stats online via coveralls.io"
name = "coveralls"
optional = false
python-versions = "*"
version = "1.8.2"
[package.dependencies]
coverage = ">=3.6,<5.0"
docopt = ">=0.6.1"
requests = ">=1.0.0"
[package.dependencies.urllib3]
python = "<3"
version = "*"
[[package]]
category = "dev"
description = "Pythonic argument parser, that will make you smile"
name = "docopt"
optional = false
python-versions = "*"
version = "0.6.2"
[[package]]
category = "main"
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
name = "filetype"
optional = false
python-versions = "*"
version = "1.0.5"
[[package]]
category = "dev"
description = "A microframework based on Werkzeug, Jinja2 and good intentions"
name = "flask"
optional = false
python-versions = "*"
version = "0.12.4"
[package.dependencies]
Jinja2 = ">=2.4"
Werkzeug = ">=0.7"
click = ">=2.0"
itsdangerous = ">=0.21"
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
marker = "python_version >= \"2.7\" and python_version < \"2.8\""
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.1"
[[package]]
category = "main"
description = "Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner."
name = "har2case"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
version = "0.3.1"
[package.dependencies]
PyYAML = "*"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.8"
[[package]]
category = "dev"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
optional = false
python-versions = "*"
version = "2.10.3"
[package.dependencies]
MarkupSafe = ">=0.23"
[[package]]
category = "main"
description = "An XPath for JSON"
name = "jsonpath"
optional = false
python-versions = "*"
version = "0.82"
[[package]]
category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "main"
description = "YAML parser and emitter for Python"
name = "pyyaml"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.1.2"
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.22.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<3.1.0"
idna = ">=2.5,<2.9"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[[package]]
category = "main"
description = "A utility belt for advanced users of python-requests"
name = "requests-toolbelt"
optional = false
python-versions = "*"
version = "0.9.1"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
version = "1.25.6"
[[package]]
category = "dev"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.16.0"
[metadata]
content-hash = "836d6dec466dfbf8a14481ed801c053a902b3fa6d8b75cf4f5aba4539c0899af"
python-versions = "~2.7 || ^3.5"
[metadata.hashes]
certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"]
chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
colorlog = ["3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42", "450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"]
coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"]
coveralls = ["9bc5a1f92682eef59f688a8f280207190d9a6afb84cef8f567fa47631a784060", "fb51cddef4bc458de347274116df15d641a735d3f0a580a9472174e2e62f408c"]
docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"]
filetype = ["17a3b885f19034da29640b083d767e0f13c2dcb5dcc267945c8b6e5a5a9013c7", "4967124d982a71700d94a08c49c4926423500e79382a92070f5ab248d44fe461"]
flask = ["2ea22336f6d388b4b242bc3abf8a01244a8aa3e236e7407469ef78c16ba355dd", "6c02dbaa5a9ef790d8219bdced392e2d549c10cd5a5ba4b6aa65126b2271af29"]
future = ["858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093"]
har2case = ["84d3a5cc9fbb16e45372e7e880a936c59bbe8e9b66bad81927769e64f608e2af", "8f159ec7cba82ec4282f46af4a9dac89f65e62796521b2426d3c89c3c9fd8579"]
idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"]
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
jsonpath = ["46d3fd2016cd5b842283d547877a02c418a0fe9aa7a6b0ae344115a2c990fef4"]
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
requests-toolbelt = ["380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"]
urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"]
werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "2.2.6" version = "2.3.0"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -17,32 +17,31 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules" "Topic :: Software Development :: Libraries :: Python Modules"
] ]
include = ["CHANGELOG.md", "httprunner/templates/*"] include = ["CHANGELOG.md", "httprunner/static/*"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.5" python = "~2.7 || ^3.5"
requests = "^2.14" requests = "^2.22.0"
requests-toolbelt = "^0.8.0" requests-toolbelt = "^0.9.1"
pyyaml = "^5.1" pyyaml = "^5.1.2"
jinja2 = "^2.10" jinja2 = "^2.10.3"
har2case = "^0.3.1" har2case = "^0.3.1"
colorama = "^0.4.1" colorama = "^0.4.1"
colorlog = "^4.0" colorlog = "^4.0.2"
filetype = "^1.0" filetype = "^1.0.5"
future = { version = "^0.17.1", python = "~2.7" }
jsonpath = "^0.82" jsonpath = "^0.82"
future = { version = "^0.18.1", python = "~2.7" }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
flask = "<1.0.0" flask = "<1.0.0"
coverage = "^4.5" coverage = "^4.5.4"
coveralls = "^1.8" coveralls = "^1.8.2"
contextlib2 = "^0.5.5"
[tool.poetry.scripts] [tool.poetry.scripts]
hrun = "httprunner.cli:main_hrun" hrun = "httprunner.cli:main"
ate = "httprunner.cli:main_hrun" ate = "httprunner.cli:main"
httprunner = "httprunner.cli:main_hrun" httprunner = "httprunner.cli:main"
locusts = "httprunner.cli:main_locust" locusts = "httprunner.plugins.locusts:main"
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]

View File

@@ -4,6 +4,7 @@ import json
from functools import wraps from functools import wraps
from flask import Flask, make_response, request from flask import Flask, make_response, request
from httprunner.built_in import gen_random_string from httprunner.built_in import gen_random_string
try: try:
@@ -53,6 +54,7 @@ def get_sign(*args):
sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest() sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest()
return sign return sign
def gen_md5(*args): def gen_md5(*args):
return hashlib.md5("".join(args).encode('utf-8')).hexdigest() return hashlib.md5("".join(args).encode('utf-8')).hexdigest()

View File

@@ -3,9 +3,10 @@ import time
import unittest import unittest
import requests import requests
from tests.api_server import FLASK_APP_PORT, HTTPBIN_HOST, HTTPBIN_PORT from tests.api_server import FLASK_APP_PORT, HTTPBIN_HOST, HTTPBIN_PORT
from tests.api_server import app as flask_app from tests.api_server import app as flask_app
from tests.api_server import gen_md5, gen_random_string, get_sign, httpbin_app from tests.api_server import gen_random_string, get_sign, httpbin_app
def run_flask(): def run_flask():

View File

@@ -1,4 +1,3 @@
import json
import os import os
import random import random
import string import string
@@ -8,12 +7,15 @@ from tests.api_server import HTTPBIN_SERVER, gen_md5, get_sign
BASE_URL = "http://127.0.0.1:5000" BASE_URL = "http://127.0.0.1:5000"
def get_httpbin_server(): def get_httpbin_server():
return HTTPBIN_SERVER return HTTPBIN_SERVER
def get_base_url(): def get_base_url():
return BASE_URL return BASE_URL
def get_default_request(): def get_default_request():
return { return {
"base_url": BASE_URL, "base_url": BASE_URL,
@@ -22,9 +24,11 @@ def get_default_request():
} }
} }
def sum_two(m, n): def sum_two(m, n):
return m + n return m + n
def sum_status_code(status_code, expect_sum): def sum_status_code(status_code, expect_sum):
""" sum status code digits """ sum status code digits
e.g. 400 => 4, 201 => 3 e.g. 400 => 4, 201 => 3
@@ -35,34 +39,42 @@ def sum_status_code(status_code, expect_sum):
assert sum_value == expect_sum assert sum_value == expect_sum
def is_status_code_200(status_code): def is_status_code_200(status_code):
return status_code == 200 return status_code == 200
os.environ["TEST_ENV"] = "PRODUCTION" os.environ["TEST_ENV"] = "PRODUCTION"
def skip_test_in_production_env(): def skip_test_in_production_env():
""" skip this test in production environment """ skip this test in production environment
""" """
return os.environ["TEST_ENV"] == "PRODUCTION" return os.environ["TEST_ENV"] == "PRODUCTION"
def get_user_agent(): def get_user_agent():
return ["iOS/10.1", "iOS/10.2"] return ["iOS/10.1", "iOS/10.2"]
def gen_app_version(): def gen_app_version():
return [ return [
{"app_version": "2.8.5"}, {"app_version": "2.8.5"},
{"app_version": "2.8.6"} {"app_version": "2.8.6"}
] ]
def get_account(): def get_account():
return [ return [
{"username": "user1", "password": "111111"}, {"username": "user1", "password": "111111"},
{"username": "user2", "password": "222222"} {"username": "user2", "password": "222222"}
] ]
def get_account_in_tuple(): def get_account_in_tuple():
return [("user1", "111111"), ("user2", "222222")] return [("user1", "111111"), ("user2", "222222")]
def gen_random_string(str_len): def gen_random_string(str_len):
random_char_list = [] random_char_list = []
for _ in range(str_len): for _ in range(str_len):
@@ -72,12 +84,15 @@ def gen_random_string(str_len):
random_string = ''.join(random_char_list) random_string = ''.join(random_char_list)
return random_string return random_string
def setup_hook_add_kwargs(request): def setup_hook_add_kwargs(request):
request["key"] = "value" request["key"] = "value"
def setup_hook_remove_kwargs(request): def setup_hook_remove_kwargs(request):
request.pop("key") request.pop("key")
def teardown_hook_sleep_N_secs(response, n_secs): def teardown_hook_sleep_N_secs(response, n_secs):
""" sleep n seconds after request """ sleep n seconds after request
""" """
@@ -86,12 +101,15 @@ def teardown_hook_sleep_N_secs(response, n_secs):
else: else:
time.sleep(n_secs) time.sleep(n_secs)
def hook_print(msg): def hook_print(msg):
print(msg) print(msg)
def modify_request_json(request, os_platform): def modify_request_json(request, os_platform):
request["json"]["os_platform"] = os_platform request["json"]["os_platform"] = os_platform
def setup_hook_httpntlmauth(request): def setup_hook_httpntlmauth(request):
if "httpntlmauth" in request: if "httpntlmauth" in request:
from requests_ntlm import HttpNtlmAuth from requests_ntlm import HttpNtlmAuth
@@ -99,6 +117,7 @@ def setup_hook_httpntlmauth(request):
request["auth"] = HttpNtlmAuth( request["auth"] = HttpNtlmAuth(
auth_account["username"], auth_account["password"]) auth_account["username"], auth_account["password"])
def alter_response(response): def alter_response(response):
response.status_code = 500 response.status_code = 500
response.headers["Content-Type"] = "html/text" response.headers["Content-Type"] = "html/text"
@@ -108,6 +127,7 @@ def alter_response(response):
"key": 123 "key": 123
} }
def alter_response_error(response): def alter_response_error(response):
# NameError # NameError
not_defined_variable not_defined_variable

View File

@@ -2,10 +2,9 @@ import os
import re import re
import shutil import shutil
import time import time
import unittest
from httprunner import exceptions, loader, parser from httprunner import exceptions, loader, parser
from httprunner.api import HttpRunner, prepare_locust_tests from httprunner.api import HttpRunner
from tests.api_server import HTTPBIN_SERVER from tests.api_server import HTTPBIN_SERVER
from tests.base import ApiServerUnittest from tests.base import ApiServerUnittest
@@ -788,18 +787,3 @@ class TestApi(ApiServerUnittest):
results.records[1]["name"], results.records[1]["name"],
"create user and check result." "create user and check result."
) )
class TestLocust(unittest.TestCase):
def test_prepare_locust_tests(self):
path = os.path.join(
os.getcwd(), 'tests/locust_tests/demo_locusts.yml')
locust_tests = prepare_locust_tests(path)
self.assertEqual(len(locust_tests), 2 + 3)
name_list = [
"create user 1000 and check result.",
"create user 1001 and check result."
]
self.assertIn(locust_tests[0]["config"]["name"], name_list)
self.assertIn(locust_tests[4]["config"]["name"], name_list)

View File

@@ -1,6 +1,3 @@
import random
import requests
from tests.base import ApiServerUnittest from tests.base import ApiServerUnittest

View File

@@ -1,5 +1,4 @@
from httprunner.client import HttpSession from httprunner.client import HttpSession
from httprunner.compat import bytes
from tests.api_server import HTTPBIN_SERVER from tests.api_server import HTTPBIN_SERVER
from tests.base import ApiServerUnittest from tests.base import ApiServerUnittest

View File

@@ -1,8 +1,8 @@
import os import os
import time
from httprunner import context, exceptions, loader, parser, runner from httprunner import context, exceptions, loader, parser, runner
from tests.base import ApiServerUnittest, gen_md5, gen_random_string from tests.api_server import gen_md5
from tests.base import ApiServerUnittest, gen_random_string
class TestContext(ApiServerUnittest): class TestContext(ApiServerUnittest):

View File

@@ -2,7 +2,7 @@
import os import os
import unittest import unittest
from httprunner import exceptions, loader, validator from httprunner import exceptions, loader
class TestFileLoader(unittest.TestCase): class TestFileLoader(unittest.TestCase):

View File

View File

@@ -0,0 +1,19 @@
import os
import unittest
from httprunner.plugins.locusts.utils import prepare_locust_tests
class TestLocust(unittest.TestCase):
def test_prepare_locust_tests(self):
path = os.path.join(
os.getcwd(), 'tests/locust_tests/demo_locusts.yml')
locust_tests = prepare_locust_tests(path)
self.assertEqual(len(locust_tests), 2 + 3)
name_list = [
"create user 1000 and check result.",
"create user 1001 and check result."
]
self.assertIn(locust_tests[0]["config"]["name"], name_list)
self.assertIn(locust_tests[4]["config"]["name"], name_list)

View File

@@ -1,4 +1,5 @@
import requests import requests
from httprunner import built_in, exceptions, loader, response from httprunner import built_in, exceptions, loader, response
from httprunner.compat import basestring, bytes from httprunner.compat import basestring, bytes
from tests.api_server import HTTPBIN_SERVER from tests.api_server import HTTPBIN_SERVER

View File

@@ -2,7 +2,7 @@ import io
import os import os
import shutil import shutil
from httprunner import exceptions, loader, parser, utils from httprunner import exceptions, loader, utils
from tests.base import ApiServerUnittest from tests.base import ApiServerUnittest