mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
logging with colors
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
import argparse
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
|
||||
from httprunner import __version__ as hrun_version
|
||||
from httprunner.utils import create_scaffold, print_output, string_type
|
||||
from pyunitreport import __version__ as pyu_version
|
||||
from pyunitreport import HTMLTestRunner
|
||||
|
||||
from . import __version__ as hrun_version
|
||||
from . import logger
|
||||
from .exception import TestcaseNotFound
|
||||
from .task import Result, TaskSuite
|
||||
from .utils import create_scaffold, print_output, string_type
|
||||
|
||||
|
||||
def run_suite_path(path, mapping=None, runner=None):
|
||||
@@ -59,21 +59,13 @@ def main_hrun():
|
||||
help="Specify new project name.")
|
||||
|
||||
args = parser.parse_args()
|
||||
logger.setup_logger(args.log_level)
|
||||
|
||||
if args.version:
|
||||
print("HttpRunner version: {}".format(hrun_version))
|
||||
print("PyUnitReport version: {}".format(pyu_version))
|
||||
logger.color_print("HttpRunner version: {}".format(hrun_version), "GREEN")
|
||||
logger.color_print("PyUnitReport version: {}".format(pyu_version), "GREEN")
|
||||
exit(0)
|
||||
|
||||
log_level = getattr(logging, args.log_level.upper(), None)
|
||||
if not log_level:
|
||||
raise ValueError('Invalid log level: %s' % args.log_level)
|
||||
logging.basicConfig(level=log_level)
|
||||
|
||||
if log_level >= 20:
|
||||
# hide traceback when log level is INFO/WARNING/ERROR/CRITICAL
|
||||
sys.tracebacklimit = 0
|
||||
|
||||
project_name = args.startproject
|
||||
if project_name:
|
||||
project_path = os.path.join(os.getcwd(), project_name)
|
||||
@@ -93,14 +85,14 @@ def main_hrun():
|
||||
def main_locust():
|
||||
""" Performance test with locust: parse command line options and run commands.
|
||||
"""
|
||||
logging.basicConfig(level="INFO")
|
||||
logger.setup_logger("INFO")
|
||||
|
||||
try:
|
||||
from httprunner import locusts
|
||||
except ImportError:
|
||||
msg = "Locust is not installed, install first and try again.\n"
|
||||
msg += "install command: pip install locustio"
|
||||
logging.info(msg)
|
||||
logger.log_warning(msg)
|
||||
exit(1)
|
||||
|
||||
sys.argv[0] = 'locust'
|
||||
@@ -115,7 +107,7 @@ def main_locust():
|
||||
testcase_index = sys.argv.index('-f') + 1
|
||||
assert testcase_index < len(sys.argv)
|
||||
except (ValueError, AssertionError):
|
||||
logging.error("Testcase file is not specified, exit.")
|
||||
logger.log_error("Testcase file is not specified, exit.")
|
||||
sys.exit(1)
|
||||
|
||||
testcase_file_path = sys.argv[testcase_index]
|
||||
@@ -125,7 +117,7 @@ def main_locust():
|
||||
""" locusts -f locustfile.py --cpu-cores 4
|
||||
"""
|
||||
if "--no-web" in sys.argv:
|
||||
logging.error("conflict parameter args: --cpu-cores & --no-web. \nexit.")
|
||||
logger.log_error("conflict parameter args: --cpu-cores & --no-web. \nexit.")
|
||||
sys.exit(1)
|
||||
|
||||
cpu_cores_index = sys.argv.index('--cpu-cores')
|
||||
@@ -137,7 +129,7 @@ def main_locust():
|
||||
locusts -f locustfile.py --cpu-cores
|
||||
"""
|
||||
cpu_cores_num_value = multiprocessing.cpu_count()
|
||||
logging.warning("cpu cores number not specified, use {} by default.".format(cpu_cores_num_value))
|
||||
logger.log_warning("cpu cores number not specified, use {} by default.".format(cpu_cores_num_value))
|
||||
else:
|
||||
try:
|
||||
""" locusts -f locustfile.py --cpu-cores 4 """
|
||||
@@ -146,7 +138,7 @@ def main_locust():
|
||||
except ValueError:
|
||||
""" locusts -f locustfile.py --cpu-cores -P 8888 """
|
||||
cpu_cores_num_value = multiprocessing.cpu_count()
|
||||
logging.warning("cpu cores number not specified, use {} by default.".format(cpu_cores_num_value))
|
||||
logger.log_warning("cpu cores number not specified, use {} by default.".format(cpu_cores_num_value))
|
||||
|
||||
sys.argv.pop(cpu_cores_index)
|
||||
locusts.run_locusts_on_cpu_cores(sys.argv, cpu_cores_num_value)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
from httprunner.exception import ParamsError
|
||||
from requests import Request, Response
|
||||
from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
|
||||
RequestException)
|
||||
|
||||
from . import logger
|
||||
from .exception import ParamsError
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
|
||||
@@ -99,8 +100,8 @@ class HttpSession(requests.Session):
|
||||
|
||||
# prepend url with hostname unless it's already an absolute URL
|
||||
url = self._build_url(url)
|
||||
logging.info(" Start to {method} {url}".format(method=method, url=url))
|
||||
logging.debug(" kwargs: {kwargs}".format(kwargs=kwargs))
|
||||
logger.log_info("{method} {url}".format(method=method, url=url))
|
||||
logger.log_debug("request kwargs: {kwargs}".format(kwargs=kwargs))
|
||||
# store meta data that is used when reporting the request to locust's statistics
|
||||
request_meta = {}
|
||||
|
||||
@@ -136,16 +137,17 @@ class HttpSession(requests.Session):
|
||||
request_meta["response_headers"] = response.headers
|
||||
request_meta["response_content"] = response.content
|
||||
|
||||
logging.debug(" response: {response}".format(response=request_meta))
|
||||
logger.log_debug("response status_code: {}".format(response.status_code))
|
||||
logger.log_debug("response headers: {}".format(response.headers))
|
||||
logger.log_debug("response body: {}".format(response.text))
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException as e:
|
||||
logging.error(u" Failed to {method} {url}! exception msg: {exception}".format(
|
||||
method=method, url=url, exception=str(e)))
|
||||
logger.log_error(u"{exception}".format(exception=str(e)))
|
||||
else:
|
||||
logging.info(
|
||||
""" status_code: {}, response_time: {} ms, response_length: {} bytes"""\
|
||||
logger.log_info(
|
||||
"""status_code: {}, response_time: {} ms, response_length: {} bytes"""\
|
||||
.format(request_meta["status_code"], request_meta["response_time"], \
|
||||
request_meta["content_size"]))
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
|
||||
from httprunner.testcase import load_test_file
|
||||
from locust.main import main
|
||||
|
||||
from .logger import color_print
|
||||
from .testcase import load_test_file
|
||||
|
||||
|
||||
def parse_locustfile(file_path):
|
||||
""" parse testcase file and return locustfile path.
|
||||
@@ -13,7 +15,7 @@ def parse_locustfile(file_path):
|
||||
if file_path is a YAML/JSON file, convert it to locustfile
|
||||
"""
|
||||
if not os.path.isfile(file_path):
|
||||
print("file path invalid, exit.")
|
||||
color_print("file path invalid, exit.", "RED")
|
||||
sys.exit(1)
|
||||
|
||||
file_suffix = os.path.splitext(file_path)[1]
|
||||
@@ -23,7 +25,7 @@ def parse_locustfile(file_path):
|
||||
locustfile_path = gen_locustfile(file_path)
|
||||
else:
|
||||
# '' or other suffix
|
||||
print("file type should be YAML/JSON/Python, exit.")
|
||||
color_print("file type should be YAML/JSON/Python, exit.", "RED")
|
||||
sys.exit(1)
|
||||
|
||||
return locustfile_path
|
||||
|
||||
63
httprunner/logger.py
Normal file
63
httprunner/logger.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from colorama import Back, Fore, Style, init
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
log_colors_config = {
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
|
||||
def setup_logger(log_level):
|
||||
"""setup root logger with ColoredFormatter."""
|
||||
level = getattr(logging, log_level.upper(), None)
|
||||
if not level:
|
||||
color_print("Invalid log level: %s" % log_level, "RED")
|
||||
sys.exit(1)
|
||||
|
||||
# hide traceback when log level is INFO/WARNING/ERROR/CRITICAL
|
||||
if level >= logging.INFO:
|
||||
sys.tracebacklimit = 0
|
||||
|
||||
formatter = ColoredFormatter(
|
||||
"%(log_color)s%(bg_white)s%(levelname)-8s%(reset)s %(message)s",
|
||||
datefmt=None,
|
||||
reset=True,
|
||||
log_colors=log_colors_config
|
||||
)
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logging.root.addHandler(handler)
|
||||
logging.root.setLevel(level)
|
||||
|
||||
|
||||
def coloring(text, color="WHITE"):
|
||||
fore_color = getattr(Fore, color.upper())
|
||||
return fore_color + text
|
||||
|
||||
def color_print(msg, color="WHITE"):
|
||||
fore_color = getattr(Fore, color.upper())
|
||||
print(fore_color + msg)
|
||||
|
||||
def log_with_color(level):
|
||||
""" log with color by different level
|
||||
"""
|
||||
def wrapper(text):
|
||||
color = log_colors_config[level.upper()]
|
||||
getattr(logging, level.lower())(coloring(text, color))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
log_debug = log_with_color("debug")
|
||||
log_info = log_with_color("info")
|
||||
log_warning = log_with_color("warning")
|
||||
log_error = log_with_color("error")
|
||||
log_critical = log_with_color("critical")
|
||||
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
from httprunner import exception, utils, testcase
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from . import exception, logger, testcase, utils
|
||||
|
||||
text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ class ResponseObject(object):
|
||||
"""
|
||||
matched = re.search(field, self.resp_text)
|
||||
if not matched:
|
||||
err_msg = u"Extractor error: failed to extract data with regex!\n"
|
||||
err_msg = u"Failed to extract data with regex!\n"
|
||||
err_msg += u"response body: {}\n".format(self.resp_text)
|
||||
err_msg += u"regex: {}\n".format(field)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.ParamsError(err_msg)
|
||||
|
||||
return matched.group(1)
|
||||
@@ -75,10 +75,10 @@ class ResponseObject(object):
|
||||
|
||||
if sub_query:
|
||||
if not isinstance(top_query_content, (dict, CaseInsensitiveDict, list)):
|
||||
err_msg = u"Extractor error: failed to extract data with regex!\n"
|
||||
err_msg = u"Failed to extract data with regex!\n"
|
||||
err_msg += u"response: {}\n".format(self.parsed_dict())
|
||||
err_msg += u"regex: {}\n".format(field)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.ParamsError(err_msg)
|
||||
|
||||
# e.g. key: resp_headers_content_type, sub_query = "content-type"
|
||||
@@ -91,7 +91,7 @@ class ResponseObject(object):
|
||||
err_msg = u"Failed to extract value from response!\n"
|
||||
err_msg += u"response: {}\n".format(self.parsed_dict())
|
||||
err_msg += u"extract field: {}\n".format(field)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.ParamsError(err_msg)
|
||||
|
||||
def extract_field(self, field):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import logging
|
||||
from unittest.case import SkipTest
|
||||
|
||||
from httprunner import exception, response, testcase, utils
|
||||
from httprunner.client import HttpSession
|
||||
from httprunner.context import Context
|
||||
from . import exception, logger, response, testcase, utils
|
||||
from .client import HttpSession
|
||||
from .context import Context
|
||||
|
||||
|
||||
class Runner(object):
|
||||
@@ -156,12 +155,20 @@ class Runner(object):
|
||||
try:
|
||||
self.context.validate(validators, resp_obj)
|
||||
except (exception.ParamsError, exception.ResponseError, exception.ValidationError):
|
||||
err_msg = u"Exception occured.\n"
|
||||
err_msg += u"HTTP request url: {}\n".format(url)
|
||||
err_msg += u"HTTP request kwargs: {}\n".format(parsed_request)
|
||||
err_msg += u"HTTP response status_code: {}\n".format(resp.status_code)
|
||||
err_msg += u"HTTP response content: \n{}".format(resp.text)
|
||||
logging.error(err_msg)
|
||||
# log request
|
||||
err_req_msg = "request: \n"
|
||||
err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {}))
|
||||
for k, v in parsed_request.items():
|
||||
err_req_msg += "{}: {}\n".format(k, v)
|
||||
logger.log_error(err_req_msg)
|
||||
|
||||
# log response
|
||||
err_resp_msg = "response: \n"
|
||||
err_resp_msg += "status_code: {}\n".format(resp.status_code)
|
||||
err_resp_msg += "headers: {}\n".format(resp.headers)
|
||||
err_resp_msg += "body: {}\n".format(resp.text)
|
||||
logger.log_error(err_resp_msg)
|
||||
|
||||
raise
|
||||
finally:
|
||||
setup_teardown(teardown_actions)
|
||||
@@ -174,7 +181,7 @@ class Runner(object):
|
||||
output = {}
|
||||
for variable in output_variables_list:
|
||||
if variable not in variables_mapping:
|
||||
logging.warning(
|
||||
logger.log_warning(
|
||||
"variable '{}' can not be found in variables mapping, failed to ouput!"\
|
||||
.format(variable)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from httprunner import exception, runner, testcase, utils
|
||||
from . import exception, logger, runner, testcase, utils
|
||||
|
||||
|
||||
class ApiTestCase(unittest.TestCase):
|
||||
@@ -73,10 +72,11 @@ class ApiTestSuite(unittest.TestSuite):
|
||||
def _add_tests_to_suite(self, testcases):
|
||||
for testcase_dict in testcases:
|
||||
testcase_name = self.test_runner.context.eval_content(testcase_dict["name"])
|
||||
testcase_name_with_color = logger.coloring(testcase_name, "yellow")
|
||||
if utils.PYTHON_VERSION == 3:
|
||||
ApiTestCase.runTest.__doc__ = testcase_name
|
||||
ApiTestCase.runTest.__doc__ = testcase_name_with_color
|
||||
else:
|
||||
ApiTestCase.runTest.__func__.__doc__ = testcase_name
|
||||
ApiTestCase.runTest.__func__.__doc__ = testcase_name_with_color
|
||||
|
||||
test = ApiTestCase(self.test_runner, testcase_dict)
|
||||
[self.addTest(test) for _ in range(int(testcase_dict.get("times", 1)))]
|
||||
@@ -165,14 +165,10 @@ class LocustTask(object):
|
||||
try:
|
||||
test.runTest()
|
||||
except exception.MyBaseError as ex:
|
||||
try:
|
||||
from locust.events import request_failure
|
||||
request_failure.fire(
|
||||
request_type=test.testcase_dict.get("request", {}).get("method"),
|
||||
name=test.testcase_dict.get("request", {}).get("url"),
|
||||
response_time=0,
|
||||
exception=ex
|
||||
)
|
||||
except ImportError:
|
||||
logging.exception(
|
||||
"Exception occured in testcase: {}".format(test.testcase_dict.get("name")))
|
||||
from locust.events import request_failure
|
||||
request_failure.fire(
|
||||
request_type=test.testcase_dict.get("request", {}).get("method"),
|
||||
name=test.testcase_dict.get("request", {}).get("url"),
|
||||
response_time=0,
|
||||
exception=ex
|
||||
)
|
||||
|
||||
@@ -2,14 +2,14 @@ import ast
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
from httprunner import exception, utils
|
||||
|
||||
from . import exception, logger, utils
|
||||
|
||||
variable_regexp = r"\$([\w_]+)"
|
||||
function_regexp = r"\$\{([\w_]+\([\$\w_ =,]*\))\}"
|
||||
@@ -38,7 +38,7 @@ def _load_json_file(json_file):
|
||||
json_content = json.load(data_file)
|
||||
except exception.JSONDecodeError:
|
||||
err_msg = u"JSONDecodeError: JSON file format error: {}".format(json_file)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.FileFormatError(err_msg)
|
||||
|
||||
check_format(json_file, json_content)
|
||||
@@ -110,8 +110,8 @@ def load_file(file_path):
|
||||
return _load_csv_file(file_path)
|
||||
else:
|
||||
# '' or other suffix
|
||||
err_msg = u"file is not in YAML/JSON format: {}".format(file_path)
|
||||
logging.warning(err_msg)
|
||||
err_msg = u"Unsupported file format: {}".format(file_path)
|
||||
logger.log_warning(err_msg)
|
||||
return []
|
||||
|
||||
def extract_variables(content):
|
||||
@@ -267,7 +267,7 @@ def load_testcases_by_path(path):
|
||||
testcases_list = []
|
||||
|
||||
else:
|
||||
logging.error(u"file not found: {}".format(path))
|
||||
logger.log_error(u"file not found: {}".format(path))
|
||||
testcases_list = []
|
||||
|
||||
testcases_cache_mapping[path] = testcases_list
|
||||
@@ -380,7 +380,7 @@ def merge_extractor(api_extrators, test_extracors):
|
||||
extractor_dict = OrderedDict()
|
||||
for api_extrator in api_extrators:
|
||||
if len(api_extrator) != 1:
|
||||
logging.warning("incorrect extractor: {}".format(api_extrator))
|
||||
logger.log_warning("incorrect extractor: {}".format(api_extrator))
|
||||
continue
|
||||
|
||||
var_name = list(api_extrator.keys())[0]
|
||||
@@ -388,7 +388,7 @@ def merge_extractor(api_extrators, test_extracors):
|
||||
|
||||
for test_extrator in test_extracors:
|
||||
if len(test_extrator) != 1:
|
||||
logging.warning("incorrect extractor: {}".format(test_extrator))
|
||||
logger.log_warning("incorrect extractor: {}".format(test_extrator))
|
||||
continue
|
||||
|
||||
var_name = list(test_extrator.keys())[0]
|
||||
@@ -601,13 +601,13 @@ def check_format(file_path, content):
|
||||
if not content:
|
||||
# testcase file content is empty
|
||||
err_msg = u"Testcase file content is empty: {}".format(file_path)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.FileFormatError(err_msg)
|
||||
|
||||
elif not isinstance(content, (list, dict)):
|
||||
# testcase file content does not match testcase format
|
||||
err_msg = u"Testcase file content format invalid: {}".format(file_path)
|
||||
logging.error(err_msg)
|
||||
logger.log_error(err_msg)
|
||||
raise exception.FileFormatError(err_msg)
|
||||
|
||||
def gen_cartesian_product(*args):
|
||||
|
||||
@@ -2,7 +2,6 @@ import hashlib
|
||||
import hmac
|
||||
import imp
|
||||
import importlib
|
||||
import logging
|
||||
import os.path
|
||||
import random
|
||||
import re
|
||||
@@ -10,9 +9,10 @@ import string
|
||||
import types
|
||||
from collections import OrderedDict
|
||||
|
||||
from httprunner import exception
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from . import exception, logger
|
||||
|
||||
try:
|
||||
string_type = basestring
|
||||
long_type = long
|
||||
@@ -351,23 +351,23 @@ def print_output(output):
|
||||
|
||||
content += "============================================\n"
|
||||
|
||||
logging.debug(content)
|
||||
logger.log_debug(content)
|
||||
|
||||
def create_scaffold(project_path):
|
||||
logging.info(" Start to create new project: {}".format(project_path))
|
||||
|
||||
if os.path.isdir(project_path):
|
||||
folder_name = os.path.basename(project_path)
|
||||
logging.warning(u" Folder {} exists, please specify a new folder name.".format(folder_name))
|
||||
logger.log_warning(u"Folder {} exists, please specify a new folder name.".format(folder_name))
|
||||
return
|
||||
|
||||
logger.color_print("Start to create new project: {}\n".format(project_path), "GREEN")
|
||||
|
||||
def create_path(path, ptype):
|
||||
if ptype == "folder":
|
||||
os.makedirs(path)
|
||||
elif ptype == "file":
|
||||
open(path, 'w').close()
|
||||
|
||||
logging.info("\tcreated {}: {}".format(ptype, path))
|
||||
return "created {}: {}\n".format(ptype, path)
|
||||
|
||||
path_list = [
|
||||
(project_path, "folder"),
|
||||
@@ -377,4 +377,9 @@ def create_scaffold(project_path):
|
||||
(os.path.join(project_path, "tests", "testcases"), "folder"),
|
||||
(os.path.join(project_path, "tests", "debugtalk.py"), "file")
|
||||
]
|
||||
[create_path(p[0], p[1]) for p in path_list]
|
||||
|
||||
msg = ""
|
||||
for p in path_list:
|
||||
msg += create_path(p[0], p[1])
|
||||
|
||||
logger.color_print(msg, "BLUE")
|
||||
|
||||
@@ -2,4 +2,6 @@ requests[security]
|
||||
flask
|
||||
PyYAML
|
||||
PyUnitReport
|
||||
har2case
|
||||
har2case
|
||||
colorama
|
||||
colorlog
|
||||
Reference in New Issue
Block a user