mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-10 23:12:41 +08:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,5 +13,5 @@ logs
|
|||||||
.coverage
|
.coverage
|
||||||
locustfile.py
|
locustfile.py
|
||||||
site/
|
site/
|
||||||
poetry.lock
|
|
||||||
reports
|
reports
|
||||||
|
.venv
|
||||||
@@ -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
|
||||||
|
|||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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**
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ HttpRunner is rich documented.
|
|||||||
|
|
||||||
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
关注 HttpRunner 的微信公众号,第一时间获得最新资讯。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[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
|
||||||
|
|||||||
@@ -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
6
httprunner/__main__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from httprunner.cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
2
httprunner/plugins/__init__.py
Normal file
2
httprunner/plugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# NOTICE:
|
||||||
|
# This file should not be deleted, or ImportError will be raised in Python 2.7 when importing plugin
|
||||||
104
httprunner/plugins/locusts/README.md
Normal file
104
httprunner/plugins/locusts/README.md
Normal 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
|
||||||
|
```
|
||||||
0
httprunner/plugins/locusts/__init__.py
Normal file
0
httprunner/plugins/locusts/__init__.py
Normal file
4
httprunner/plugins/locusts/__main__.py
Normal file
4
httprunner/plugins/locusts/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from httprunner.plugins.locusts.cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
169
httprunner/plugins/locusts/cli.py
Normal file
169
httprunner/plugins/locusts/cli.py
Normal 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()
|
||||||
@@ -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)
|
||||||
29
httprunner/plugins/locusts/utils.py
Normal file
29
httprunner/plugins/locusts/utils.py
Normal 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
|
||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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
237
poetry.lock
generated
Normal 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"]
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import random
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from tests.base import ApiServerUnittest
|
from tests.base import ApiServerUnittest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
0
tests/test_plugins/__init__.py
Normal file
0
tests/test_plugins/__init__.py
Normal file
19
tests/test_plugins/test_locusts.py
Normal file
19
tests/test_plugins/test_locusts.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user