logging with colors

This commit is contained in:
debugtalk
2018-02-21 12:32:14 +08:00
parent 967cfeeb8f
commit 7c2b9a6eaa
10 changed files with 153 additions and 84 deletions

View File

@@ -1,18 +1,18 @@
import argparse import argparse
import logging
import multiprocessing import multiprocessing
import os import os
import sys import sys
import unittest import unittest
from collections import OrderedDict 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 __version__ as pyu_version
from pyunitreport import HTMLTestRunner from pyunitreport import HTMLTestRunner
from . import __version__ as hrun_version
from . import logger
from .exception import TestcaseNotFound from .exception import TestcaseNotFound
from .task import Result, TaskSuite from .task import Result, TaskSuite
from .utils import create_scaffold, print_output, string_type
def run_suite_path(path, mapping=None, runner=None): def run_suite_path(path, mapping=None, runner=None):
@@ -59,21 +59,13 @@ def main_hrun():
help="Specify new project name.") help="Specify new project name.")
args = parser.parse_args() args = parser.parse_args()
logger.setup_logger(args.log_level)
if args.version: if args.version:
print("HttpRunner version: {}".format(hrun_version)) logger.color_print("HttpRunner version: {}".format(hrun_version), "GREEN")
print("PyUnitReport version: {}".format(pyu_version)) logger.color_print("PyUnitReport version: {}".format(pyu_version), "GREEN")
exit(0) 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 project_name = args.startproject
if project_name: if project_name:
project_path = os.path.join(os.getcwd(), project_name) project_path = os.path.join(os.getcwd(), project_name)
@@ -93,14 +85,14 @@ def main_hrun():
def main_locust(): def main_locust():
""" Performance test with locust: parse command line options and run commands. """ Performance test with locust: parse command line options and run commands.
""" """
logging.basicConfig(level="INFO") logger.setup_logger("INFO")
try: try:
from httprunner import locusts from httprunner import locusts
except ImportError: except ImportError:
msg = "Locust is not installed, install first and try again.\n" msg = "Locust is not installed, install first and try again.\n"
msg += "install command: pip install locustio" msg += "install command: pip install locustio"
logging.info(msg) logger.log_warning(msg)
exit(1) exit(1)
sys.argv[0] = 'locust' sys.argv[0] = 'locust'
@@ -115,7 +107,7 @@ def main_locust():
testcase_index = sys.argv.index('-f') + 1 testcase_index = sys.argv.index('-f') + 1
assert testcase_index < len(sys.argv) assert testcase_index < len(sys.argv)
except (ValueError, AssertionError): except (ValueError, AssertionError):
logging.error("Testcase file is not specified, exit.") logger.log_error("Testcase file is not specified, exit.")
sys.exit(1) sys.exit(1)
testcase_file_path = sys.argv[testcase_index] testcase_file_path = sys.argv[testcase_index]
@@ -125,7 +117,7 @@ def main_locust():
""" locusts -f locustfile.py --cpu-cores 4 """ locusts -f locustfile.py --cpu-cores 4
""" """
if "--no-web" in sys.argv: 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) sys.exit(1)
cpu_cores_index = sys.argv.index('--cpu-cores') cpu_cores_index = sys.argv.index('--cpu-cores')
@@ -137,7 +129,7 @@ def main_locust():
locusts -f locustfile.py --cpu-cores locusts -f locustfile.py --cpu-cores
""" """
cpu_cores_num_value = multiprocessing.cpu_count() 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: else:
try: try:
""" locusts -f locustfile.py --cpu-cores 4 """ """ locusts -f locustfile.py --cpu-cores 4 """
@@ -146,7 +138,7 @@ def main_locust():
except ValueError: except ValueError:
""" locusts -f locustfile.py --cpu-cores -P 8888 """ """ locusts -f locustfile.py --cpu-cores -P 8888 """
cpu_cores_num_value = multiprocessing.cpu_count() 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) sys.argv.pop(cpu_cores_index)
locusts.run_locusts_on_cpu_cores(sys.argv, cpu_cores_num_value) locusts.run_locusts_on_cpu_cores(sys.argv, cpu_cores_num_value)

View File

@@ -1,15 +1,16 @@
import json import json
import logging
import re import re
import time import time
import requests import requests
import urllib3 import urllib3
from httprunner.exception import ParamsError
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 . import logger
from .exception import ParamsError
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
absolute_http_url_regexp = re.compile(r"^https?://", re.I) 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 # prepend url with hostname unless it's already an absolute URL
url = self._build_url(url) url = self._build_url(url)
logging.info(" Start to {method} {url}".format(method=method, url=url)) logger.log_info("{method} {url}".format(method=method, url=url))
logging.debug(" kwargs: {kwargs}".format(kwargs=kwargs)) logger.log_debug("request kwargs: {kwargs}".format(kwargs=kwargs))
# store meta data that is used when reporting the request to locust's statistics # store meta data that is used when reporting the request to locust's statistics
request_meta = {} request_meta = {}
@@ -136,16 +137,17 @@ class HttpSession(requests.Session):
request_meta["response_headers"] = response.headers request_meta["response_headers"] = response.headers
request_meta["response_content"] = response.content 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: try:
response.raise_for_status() response.raise_for_status()
except RequestException as e: except RequestException as e:
logging.error(u" Failed to {method} {url}! exception msg: {exception}".format( logger.log_error(u"{exception}".format(exception=str(e)))
method=method, url=url, exception=str(e)))
else: else:
logging.info( logger.log_info(
""" status_code: {}, response_time: {} ms, response_length: {} bytes"""\ """status_code: {}, response_time: {} ms, response_length: {} bytes"""\
.format(request_meta["status_code"], request_meta["response_time"], \ .format(request_meta["status_code"], request_meta["response_time"], \
request_meta["content_size"])) request_meta["content_size"]))

View File

@@ -3,9 +3,11 @@ import multiprocessing
import os import os
import sys import sys
from httprunner.testcase import load_test_file
from locust.main import main from locust.main import main
from .logger import color_print
from .testcase import load_test_file
def parse_locustfile(file_path): def parse_locustfile(file_path):
""" parse testcase file and return locustfile 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 file_path is a YAML/JSON file, convert it to locustfile
""" """
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
print("file path invalid, exit.") color_print("file path invalid, exit.", "RED")
sys.exit(1) sys.exit(1)
file_suffix = os.path.splitext(file_path)[1] file_suffix = os.path.splitext(file_path)[1]
@@ -23,7 +25,7 @@ def parse_locustfile(file_path):
locustfile_path = gen_locustfile(file_path) locustfile_path = gen_locustfile(file_path)
else: else:
# '' or other suffix # '' 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) sys.exit(1)
return locustfile_path return locustfile_path

63
httprunner/logger.py Normal file
View 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")

View File

@@ -1,10 +1,10 @@
import logging
import re import re
from collections import OrderedDict from collections import OrderedDict
from httprunner import exception, utils, testcase
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from . import exception, logger, testcase, utils
text_extractor_regexp_compile = re.compile(r".*\(.*\).*") text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
@@ -42,10 +42,10 @@ class ResponseObject(object):
""" """
matched = re.search(field, self.resp_text) matched = re.search(field, self.resp_text)
if not matched: 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"response body: {}\n".format(self.resp_text)
err_msg += u"regex: {}\n".format(field) err_msg += u"regex: {}\n".format(field)
logging.error(err_msg) logger.log_error(err_msg)
raise exception.ParamsError(err_msg) raise exception.ParamsError(err_msg)
return matched.group(1) return matched.group(1)
@@ -75,10 +75,10 @@ class ResponseObject(object):
if sub_query: if sub_query:
if not isinstance(top_query_content, (dict, CaseInsensitiveDict, list)): 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"response: {}\n".format(self.parsed_dict())
err_msg += u"regex: {}\n".format(field) err_msg += u"regex: {}\n".format(field)
logging.error(err_msg) logger.log_error(err_msg)
raise exception.ParamsError(err_msg) raise exception.ParamsError(err_msg)
# e.g. key: resp_headers_content_type, sub_query = "content-type" # 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"Failed to extract value from response!\n"
err_msg += u"response: {}\n".format(self.parsed_dict()) err_msg += u"response: {}\n".format(self.parsed_dict())
err_msg += u"extract field: {}\n".format(field) err_msg += u"extract field: {}\n".format(field)
logging.error(err_msg) logger.log_error(err_msg)
raise exception.ParamsError(err_msg) raise exception.ParamsError(err_msg)
def extract_field(self, field): def extract_field(self, field):

View File

@@ -1,9 +1,8 @@
import logging
from unittest.case import SkipTest from unittest.case import SkipTest
from httprunner import exception, response, testcase, utils from . import exception, logger, response, testcase, utils
from httprunner.client import HttpSession from .client import HttpSession
from httprunner.context import Context from .context import Context
class Runner(object): class Runner(object):
@@ -156,12 +155,20 @@ class Runner(object):
try: try:
self.context.validate(validators, resp_obj) self.context.validate(validators, resp_obj)
except (exception.ParamsError, exception.ResponseError, exception.ValidationError): except (exception.ParamsError, exception.ResponseError, exception.ValidationError):
err_msg = u"Exception occured.\n" # log request
err_msg += u"HTTP request url: {}\n".format(url) err_req_msg = "request: \n"
err_msg += u"HTTP request kwargs: {}\n".format(parsed_request) err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {}))
err_msg += u"HTTP response status_code: {}\n".format(resp.status_code) for k, v in parsed_request.items():
err_msg += u"HTTP response content: \n{}".format(resp.text) err_req_msg += "{}: {}\n".format(k, v)
logging.error(err_msg) 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 raise
finally: finally:
setup_teardown(teardown_actions) setup_teardown(teardown_actions)
@@ -174,7 +181,7 @@ class Runner(object):
output = {} output = {}
for variable in output_variables_list: for variable in output_variables_list:
if variable not in variables_mapping: if variable not in variables_mapping:
logging.warning( logger.log_warning(
"variable '{}' can not be found in variables mapping, failed to ouput!"\ "variable '{}' can not be found in variables mapping, failed to ouput!"\
.format(variable) .format(variable)
) )

View File

@@ -1,7 +1,6 @@
import logging
import unittest import unittest
from httprunner import exception, runner, testcase, utils from . import exception, logger, runner, testcase, utils
class ApiTestCase(unittest.TestCase): class ApiTestCase(unittest.TestCase):
@@ -73,10 +72,11 @@ class ApiTestSuite(unittest.TestSuite):
def _add_tests_to_suite(self, testcases): def _add_tests_to_suite(self, testcases):
for testcase_dict in testcases: for testcase_dict in testcases:
testcase_name = self.test_runner.context.eval_content(testcase_dict["name"]) 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: if utils.PYTHON_VERSION == 3:
ApiTestCase.runTest.__doc__ = testcase_name ApiTestCase.runTest.__doc__ = testcase_name_with_color
else: else:
ApiTestCase.runTest.__func__.__doc__ = testcase_name ApiTestCase.runTest.__func__.__doc__ = testcase_name_with_color
test = ApiTestCase(self.test_runner, testcase_dict) test = ApiTestCase(self.test_runner, testcase_dict)
[self.addTest(test) for _ in range(int(testcase_dict.get("times", 1)))] [self.addTest(test) for _ in range(int(testcase_dict.get("times", 1)))]
@@ -165,14 +165,10 @@ class LocustTask(object):
try: try:
test.runTest() test.runTest()
except exception.MyBaseError as ex: except exception.MyBaseError as ex:
try: from locust.events import request_failure
from locust.events import request_failure request_failure.fire(
request_failure.fire( request_type=test.testcase_dict.get("request", {}).get("method"),
request_type=test.testcase_dict.get("request", {}).get("method"), name=test.testcase_dict.get("request", {}).get("url"),
name=test.testcase_dict.get("request", {}).get("url"), response_time=0,
response_time=0, exception=ex
exception=ex )
)
except ImportError:
logging.exception(
"Exception occured in testcase: {}".format(test.testcase_dict.get("name")))

View File

@@ -2,14 +2,14 @@ import ast
import io import io
import itertools import itertools
import json import json
import logging
import os import os
import random import random
import re import re
from collections import OrderedDict from collections import OrderedDict
import yaml import yaml
from httprunner import exception, utils
from . import exception, logger, utils
variable_regexp = r"\$([\w_]+)" variable_regexp = r"\$([\w_]+)"
function_regexp = r"\$\{([\w_]+\([\$\w_ =,]*\))\}" function_regexp = r"\$\{([\w_]+\([\$\w_ =,]*\))\}"
@@ -38,7 +38,7 @@ def _load_json_file(json_file):
json_content = json.load(data_file) json_content = json.load(data_file)
except exception.JSONDecodeError: except exception.JSONDecodeError:
err_msg = u"JSONDecodeError: JSON file format error: {}".format(json_file) 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) raise exception.FileFormatError(err_msg)
check_format(json_file, json_content) check_format(json_file, json_content)
@@ -110,8 +110,8 @@ def load_file(file_path):
return _load_csv_file(file_path) return _load_csv_file(file_path)
else: else:
# '' or other suffix # '' or other suffix
err_msg = u"file is not in YAML/JSON format: {}".format(file_path) err_msg = u"Unsupported file format: {}".format(file_path)
logging.warning(err_msg) logger.log_warning(err_msg)
return [] return []
def extract_variables(content): def extract_variables(content):
@@ -267,7 +267,7 @@ def load_testcases_by_path(path):
testcases_list = [] testcases_list = []
else: else:
logging.error(u"file not found: {}".format(path)) logger.log_error(u"file not found: {}".format(path))
testcases_list = [] testcases_list = []
testcases_cache_mapping[path] = testcases_list testcases_cache_mapping[path] = testcases_list
@@ -380,7 +380,7 @@ def merge_extractor(api_extrators, test_extracors):
extractor_dict = OrderedDict() extractor_dict = OrderedDict()
for api_extrator in api_extrators: for api_extrator in api_extrators:
if len(api_extrator) != 1: if len(api_extrator) != 1:
logging.warning("incorrect extractor: {}".format(api_extrator)) logger.log_warning("incorrect extractor: {}".format(api_extrator))
continue continue
var_name = list(api_extrator.keys())[0] var_name = list(api_extrator.keys())[0]
@@ -388,7 +388,7 @@ def merge_extractor(api_extrators, test_extracors):
for test_extrator in test_extracors: for test_extrator in test_extracors:
if len(test_extrator) != 1: if len(test_extrator) != 1:
logging.warning("incorrect extractor: {}".format(test_extrator)) logger.log_warning("incorrect extractor: {}".format(test_extrator))
continue continue
var_name = list(test_extrator.keys())[0] var_name = list(test_extrator.keys())[0]
@@ -601,13 +601,13 @@ def check_format(file_path, content):
if not content: if not content:
# testcase file content is empty # testcase file content is empty
err_msg = u"Testcase file content is empty: {}".format(file_path) 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) raise exception.FileFormatError(err_msg)
elif not isinstance(content, (list, dict)): elif not isinstance(content, (list, dict)):
# testcase file content does not match testcase format # testcase file content does not match testcase format
err_msg = u"Testcase file content format invalid: {}".format(file_path) 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) raise exception.FileFormatError(err_msg)
def gen_cartesian_product(*args): def gen_cartesian_product(*args):

View File

@@ -2,7 +2,6 @@ import hashlib
import hmac import hmac
import imp import imp
import importlib import importlib
import logging
import os.path import os.path
import random import random
import re import re
@@ -10,9 +9,10 @@ import string
import types import types
from collections import OrderedDict from collections import OrderedDict
from httprunner import exception
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from . import exception, logger
try: try:
string_type = basestring string_type = basestring
long_type = long long_type = long
@@ -351,23 +351,23 @@ def print_output(output):
content += "============================================\n" content += "============================================\n"
logging.debug(content) logger.log_debug(content)
def create_scaffold(project_path): def create_scaffold(project_path):
logging.info(" Start to create new project: {}".format(project_path))
if os.path.isdir(project_path): if os.path.isdir(project_path):
folder_name = os.path.basename(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 return
logger.color_print("Start to create new project: {}\n".format(project_path), "GREEN")
def create_path(path, ptype): def create_path(path, ptype):
if ptype == "folder": if ptype == "folder":
os.makedirs(path) os.makedirs(path)
elif ptype == "file": elif ptype == "file":
open(path, 'w').close() open(path, 'w').close()
logging.info("\tcreated {}: {}".format(ptype, path)) return "created {}: {}\n".format(ptype, path)
path_list = [ path_list = [
(project_path, "folder"), (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", "testcases"), "folder"),
(os.path.join(project_path, "tests", "debugtalk.py"), "file") (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")

View File

@@ -3,3 +3,5 @@ flask
PyYAML PyYAML
PyUnitReport PyUnitReport
har2case har2case
colorama
colorlog