From 7c2b9a6eaad448bd1a8beec6e4251d9f5518ea52 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Feb 2018 12:32:14 +0800 Subject: [PATCH] logging with colors --- httprunner/cli.py | 32 ++++++++------------- httprunner/client.py | 20 ++++++++------ httprunner/locusts.py | 8 ++++-- httprunner/logger.py | 63 ++++++++++++++++++++++++++++++++++++++++++ httprunner/response.py | 14 +++++----- httprunner/runner.py | 29 +++++++++++-------- httprunner/task.py | 26 ++++++++--------- httprunner/testcase.py | 20 +++++++------- httprunner/utils.py | 21 ++++++++------ requirements.txt | 4 ++- 10 files changed, 153 insertions(+), 84 deletions(-) create mode 100644 httprunner/logger.py diff --git a/httprunner/cli.py b/httprunner/cli.py index 3e0d023f..f8fdece1 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -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) diff --git a/httprunner/client.py b/httprunner/client.py index 0da64566..6d65d2c5 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -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"])) diff --git a/httprunner/locusts.py b/httprunner/locusts.py index 53c02add..9bcdc459 100644 --- a/httprunner/locusts.py +++ b/httprunner/locusts.py @@ -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 diff --git a/httprunner/logger.py b/httprunner/logger.py new file mode 100644 index 00000000..1093d32c --- /dev/null +++ b/httprunner/logger.py @@ -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") diff --git a/httprunner/response.py b/httprunner/response.py index 0d8d5564..5f006d12 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -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): diff --git a/httprunner/runner.py b/httprunner/runner.py index 52326219..4a777577 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -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) ) diff --git a/httprunner/task.py b/httprunner/task.py index fe7a187e..48fabdde 100644 --- a/httprunner/task.py +++ b/httprunner/task.py @@ -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 + ) diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 9c237f47..7fd441a7 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -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): diff --git a/httprunner/utils.py b/httprunner/utils.py index f515f70a..7abc2e79 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -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") diff --git a/requirements.txt b/requirements.txt index 3ce2fa4f..a285f256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ requests[security] flask PyYAML PyUnitReport -har2case \ No newline at end of file +har2case +colorama +colorlog \ No newline at end of file