From cc83ac9853e0e7fffc448c881b70baf764e0dc74 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 10:19:32 +0800 Subject: [PATCH 01/20] change: remove unused import --- httprunner/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 71d2df85..5da791d5 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -2,15 +2,12 @@ __version__ = "3.0.7" __description__ = "One-stop solution for HTTP(S) testing." from httprunner.runner import HttpRunner -from httprunner.schema import TConfig, TStep from httprunner.testcase import Config, Step, RunRequest, RunTestCase __all__ = [ "__version__", "__description__", "HttpRunner", - "TConfig", - "TStep", "Config", "Step", "RunRequest", From 00b437620dcb9dec3adf58b4dcbfb0f7f0e228f1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 10:35:23 +0800 Subject: [PATCH 02/20] change: add httprunner version in generated pytest file --- examples/httpbin/basic_test.py | 2 +- examples/httpbin/hooks_test.py | 2 +- examples/httpbin/load_image_test.py | 2 +- examples/httpbin/upload_test.py | 2 +- examples/httpbin/validate_test.py | 2 +- .../demo_testsuite_yml/request_with_functions_test.py | 2 +- .../request_with_testcase_reference_test.py | 2 +- examples/postman_echo/request_methods/hardcode_test.py | 2 +- .../request_methods/request_with_functions_test.py | 2 +- .../request_methods/request_with_testcase_reference_test.py | 2 +- .../request_methods/request_with_variables_test.py | 2 +- .../request_methods/validate_with_functions_test.py | 2 +- .../request_methods/validate_with_variables_test.py | 2 +- httprunner/make.py | 5 +++-- 14 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index 37741c07..d09a946b 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/httpbin/basic.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index 01ebf292..7b917908 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/httpbin/hooks.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index 9e36a4f8..c59e7274 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/httpbin/load_image.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index 64dfcdbd..08b22543 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/httpbin/upload.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index c6ae7099..018406fb 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/httpbin/validate.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py index 323fb401..2057e7b7 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py index 01547709..85dd7423 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index 3d30e666..9fbfcd9f 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/hardcode.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py index 279fa9ed..478bf7b3 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py index 2fa0b1c3..2c606542 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os diff --git a/examples/postman_echo/request_methods/request_with_variables_test.py b/examples/postman_echo/request_methods/request_with_variables_test.py index 4fe8e4d4..dcbcf46d 100644 --- a/examples/postman_echo/request_methods/request_with_variables_test.py +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/request_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/validate_with_functions_test.py b/examples/postman_echo/request_methods/validate_with_functions_test.py index b203c16c..f4495555 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/validate_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/validate_with_variables_test.py b/examples/postman_echo/request_methods/validate_with_variables_test.py index 7a46d80d..d0db20af 100644 --- a/examples/postman_echo/request_methods/validate_with_variables_test.py +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner. +# NOTICE: Generated By HttpRunner v3.0.7 # FROM: examples/postman_echo/request_methods/validate_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/httprunner/make.py b/httprunner/make.py index b5fe6fd6..67cc4771 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -6,7 +6,7 @@ from typing import Text, List, Tuple, Dict, Set, NoReturn import jinja2 from loguru import logger -from httprunner import exceptions +from httprunner import exceptions, __version__ from httprunner.compat import ensure_testcase_v3_api, ensure_testcase_v3 from httprunner.loader import ( load_folder_files, @@ -24,7 +24,7 @@ make_files_cache_set: Set = set() pytest_files_set: Set = set() __TEMPLATE__ = jinja2.Template( - """# NOTICE: Generated By HttpRunner. + """# NOTICE: Generated By HttpRunner v{{ version }} # FROM: {{ testcase_path }} {% if imports_list %} import os @@ -298,6 +298,7 @@ def make_testcase( ) data = { + "version": __version__, "testcase_path": __ensure_cwd_relative(testcase_path), "class_name": f"TestCase{testcase_cls_name}", "imports_list": imports_list, From 6506a2e86422dd4c1fe08af6a2d38457ee71bd29 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 11:12:47 +0800 Subject: [PATCH 03/20] feat: add sentry sdk --- docs/CHANGELOG.md | 10 +++++++++ httprunner/cli.py | 5 +++++ httprunner/ext/har2case/__init__.py | 2 ++ httprunner/scaffold.py | 2 ++ httprunner/utils.py | 11 ++++++++++ poetry.lock | 32 ++++++++++++++++++++++++++++- pyproject.toml | 1 + 7 files changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5ad8d360..4fca7a0c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,15 @@ # Release History +## 3.0.7 (2020-06-04) + +**Added** + +- feat: add sentry sdk + +**Changed** + +- change: add httprunner version in generated pytest file + ## 3.0.7 (2020-06-03) **Added** diff --git a/httprunner/cli.py b/httprunner/cli.py index 5297074d..0e2b7ae2 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -4,12 +4,16 @@ import sys import pytest from loguru import logger +from sentry_sdk import capture_message from httprunner import __description__, __version__ from httprunner.compat import ensure_cli_args from httprunner.ext.har2case import init_har2case_parser, main_har2case from httprunner.make import init_make_parser, main_make from httprunner.scaffold import init_parser_scaffold, main_scaffold +from httprunner.utils import init_sentry_sdk + +init_sentry_sdk() def init_parser_run(subparsers): @@ -20,6 +24,7 @@ def init_parser_run(subparsers): def main_run(extra_args): + capture_message("start to run") # keep compatibility with v2 extra_args = ensure_cli_args(extra_args) diff --git a/httprunner/ext/har2case/__init__.py b/httprunner/ext/har2case/__init__.py index ade2ac36..3e5c21c4 100644 --- a/httprunner/ext/har2case/__init__.py +++ b/httprunner/ext/har2case/__init__.py @@ -12,6 +12,7 @@ import os import sys from loguru import logger +from sentry_sdk import capture_message from httprunner.ext.har2case.core import HarParser @@ -69,6 +70,7 @@ def main_har2case(args): else: output_file_type = "pytest" + capture_message(f"har2case {output_file_type}") HarParser(har_source_file, args.filter, args.exclude).gen_testcase(output_file_type) return 0 diff --git a/httprunner/scaffold.py b/httprunner/scaffold.py index bf48893f..19450d6e 100644 --- a/httprunner/scaffold.py +++ b/httprunner/scaffold.py @@ -2,6 +2,7 @@ import os.path import sys from loguru import logger +from sentry_sdk import capture_message def init_parser_scaffold(subparsers): @@ -140,5 +141,6 @@ def sleep(n_secs): def main_scaffold(args): + capture_message("startproject with scaffold") create_scaffold(args.project_name) sys.exit(0) diff --git a/httprunner/utils.py b/httprunner/utils.py index d035b3d6..3b206eea 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -2,14 +2,25 @@ import collections import json import os.path import platform +import uuid from typing import Dict, List, Any +import sentry_sdk from loguru import logger from httprunner import __version__ from httprunner import exceptions +def init_sentry_sdk(): + sentry_sdk.init( + dsn="https://460e31339bcb428c879aafa6a2e78098@sentry.io/5263855", + release="httprunner@{}".format(__version__), + ) + with sentry_sdk.configure_scope() as scope: + scope.set_user({"id": uuid.getnode()}) + + def set_os_environ(variables_mapping): """ set variables mapping to os.environ """ diff --git a/poetry.lock b/poetry.lock index 39a6f593..26dd749a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -451,6 +451,32 @@ version = "0.9.1" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +category = "main" +description = "Python client for Sentry (https://getsentry.com)" +name = "sentry-sdk" +optional = false +python-versions = "*" +version = "0.14.4" + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.10.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +flask = ["flask (>=0.11)", "blinker (>=1.1)"] +pyspark = ["pyspark (>=2.4.4)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +tornado = ["tornado (>=5)"] + [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" @@ -572,7 +598,7 @@ allure = ["allure-pytest"] upload = ["requests-toolbelt", "filetype"] [metadata] -content-hash = "3b5147c8c95480574c9eaa8f035c536cf18535766f60f768d2e714b257511dae" +content-hash = "581cacf33c8afe330e5b6a965d5e16f6266718249cbdfed0d080b7536c5c4590" python-versions = "^3.6" [metadata.files] @@ -850,6 +876,10 @@ requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] +sentry-sdk = [ + {file = "sentry-sdk-0.14.4.tar.gz", hash = "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c"}, + {file = "sentry_sdk-0.14.4-py2.py3-none-any.whl", hash = "sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c"}, +] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, diff --git a/pyproject.toml b/pyproject.toml index 083c1725..7db3053d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ jmespath = "^0.9.5" black = "^19.10b0" pytest = "^5.4.2" pytest-html = "^2.1.1" +sentry-sdk = "^0.14.4" allure-pytest = {version = "^2.8.16", optional = true} requests-toolbelt = {version = "^0.9.1", optional = true} filetype = {version = "^1.0.7", optional = true} From dd61c437debf33c6f0a7c47739bac925d7529287 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 11:24:47 +0800 Subject: [PATCH 04/20] change: replace logging with loguru --- httprunner/app/routers/debugtalk.py | 4 ++-- httprunner/app/routers/deps.py | 4 ++-- httprunner/ext/har2case/utils.py | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/httprunner/app/routers/debugtalk.py b/httprunner/app/routers/debugtalk.py index bdb9a6b9..eec92599 100644 --- a/httprunner/app/routers/debugtalk.py +++ b/httprunner/app/routers/debugtalk.py @@ -1,9 +1,9 @@ import contextlib -import logging import sys from io import StringIO from fastapi import APIRouter +from loguru import logger from starlette.requests import Request router = APIRouter() @@ -37,6 +37,6 @@ async def debug_python(request: Request): resp["code"] = 1 resp["message"] = "fail" resp["result"] = str(ex) - logging.error(resp) + logger.error(resp) return resp diff --git a/httprunner/app/routers/deps.py b/httprunner/app/routers/deps.py index 70e0a017..b83ec3ee 100644 --- a/httprunner/app/routers/deps.py +++ b/httprunner/app/routers/deps.py @@ -1,9 +1,9 @@ -import logging import subprocess from typing import List import pkg_resources from fastapi import APIRouter +from loguru import logger router = APIRouter() @@ -29,6 +29,6 @@ async def install_dependenies(deps: List[str]): resp["result"][dep] = False resp["code"] = 1 resp["message"] = "fail" - logging.error(f"failed to install dependency: {dep}") + logger.error(f"failed to install dependency: {dep}") return resp diff --git a/httprunner/ext/har2case/utils.py b/httprunner/ext/har2case/utils.py index f2fdd852..63b93530 100644 --- a/httprunner/ext/har2case/utils.py +++ b/httprunner/ext/har2case/utils.py @@ -1,11 +1,11 @@ import io import json -import logging import sys from json.decoder import JSONDecodeError from urllib.parse import unquote import yaml +from loguru import logger def load_har_log_entries(file_path): @@ -33,7 +33,7 @@ def load_har_log_entries(file_path): content_json = json.loads(f.read()) return content_json["log"]["entries"] except (KeyError, TypeError, JSONDecodeError): - logging.error("HAR file content error: {}".format(file_path)) + logger.error("HAR file content error: {}".format(file_path)) sys.exit(1) @@ -103,20 +103,20 @@ def convert_list_to_dict(origin_list): def dump_yaml(testcase, yaml_file): """ dump HAR entries to yaml testcase """ - logging.info("dump testcase to YAML format.") + logger.info("dump testcase to YAML format.") with io.open(yaml_file, "w", encoding="utf-8") as outfile: yaml.dump( testcase, outfile, allow_unicode=True, default_flow_style=False, indent=4 ) - logging.info("Generate YAML testcase successfully: {}".format(yaml_file)) + logger.info("Generate YAML testcase successfully: {}".format(yaml_file)) def dump_json(testcase, json_file): """ dump HAR entries to json testcase """ - logging.info("dump testcase to JSON format.") + logger.info("dump testcase to JSON format.") with io.open(json_file, "w", encoding="utf-8") as outfile: my_json_str = json.dumps(testcase, ensure_ascii=False, indent=4) @@ -125,4 +125,4 @@ def dump_json(testcase, json_file): outfile.write(my_json_str) - logging.info("Generate JSON testcase successfully: {}".format(json_file)) + logger.info("Generate JSON testcase successfully: {}".format(json_file)) From 1d0df3881fec75ee8ae454d453a8eecabb682cba Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 12:11:27 +0800 Subject: [PATCH 05/20] change: capture exception with sentry --- httprunner/__init__.py | 2 +- httprunner/client.py | 13 +++++++------ httprunner/ext/har2case/core.py | 15 +++++++++++---- httprunner/ext/har2case/utils.py | 4 +++- httprunner/make.py | 2 ++ httprunner/parser.py | 8 ++++++-- pyproject.toml | 2 +- 7 files changed, 31 insertions(+), 15 deletions(-) diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 5da791d5..5f9324f6 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.0.7" +__version__ = "3.0.8" __description__ = "One-stop solution for HTTP(S) testing." from httprunner.runner import HttpRunner diff --git a/httprunner/client.py b/httprunner/client.py index e6116bb7..d3cace95 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -11,6 +11,7 @@ from requests.exceptions import ( MissingSchema, RequestException, ) +from sentry_sdk import capture_exception from httprunner.schema import RequestData, ResponseData from httprunner.schema import SessionData, ReqRespData @@ -45,15 +46,15 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: request_body = resp_obj.request.body try: request_body = json.loads(request_body) - except json.JSONDecodeError: + except json.JSONDecodeError as ex: # str: Unexpected UTF-8 BOM (decode using utf-8-sig) - pass - except UnicodeDecodeError: + capture_exception(ex) + except UnicodeDecodeError as ex: # bytes/bytearray: request body in protobuf - pass - except TypeError: + capture_exception(ex) + except TypeError as ex: # neither str nor bytes/bytearray, e.g. None - pass + capture_exception(ex) if request_body: request_content_type = lower_dict_keys(request_headers).get("content-type") diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index 56d14464..5834c696 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -5,6 +5,7 @@ import sys import urllib.parse as urlparse from loguru import logger +from sentry_sdk import capture_exception from httprunner.ext.har2case import utils from httprunner.make import make_testcase, format_pytest_with_black @@ -166,8 +167,8 @@ class HarParser(object): try: post_data = json.loads(post_data) request_data_key = "json" - except JSONDecodeError: - pass + except JSONDecodeError as ex: + capture_exception(ex) elif mimeType.startswith("application/x-www-form-urlencoded"): post_data = utils.convert_x_www_form_urlencoded_to_dict(post_data) else: @@ -237,7 +238,8 @@ class HarParser(object): try: resp_content_json = json.loads(content) - except JSONDecodeError: + except JSONDecodeError as ex: + capture_exception(ex) logger.warning( "response content can not be loaded as json: {}".format( content.encode("utf-8") @@ -334,7 +336,12 @@ class HarParser(object): logger.info(f"Start to generate testcase from {self.har_file_path}") harfile = os.path.splitext(self.har_file_path)[0] - testcase = self._make_testcase() + try: + testcase = self._make_testcase() + except Exception as ex: + capture_exception(ex) + raise + logger.debug("prepared testcase: {}".format(testcase)) if file_type == "JSON": diff --git a/httprunner/ext/har2case/utils.py b/httprunner/ext/har2case/utils.py index 63b93530..cc051246 100644 --- a/httprunner/ext/har2case/utils.py +++ b/httprunner/ext/har2case/utils.py @@ -6,6 +6,7 @@ from urllib.parse import unquote import yaml from loguru import logger +from sentry_sdk import capture_exception def load_har_log_entries(file_path): @@ -32,7 +33,8 @@ def load_har_log_entries(file_path): try: content_json = json.loads(f.read()) return content_json["log"]["entries"] - except (KeyError, TypeError, JSONDecodeError): + except (KeyError, TypeError, JSONDecodeError) as ex: + capture_exception(ex) logger.error("HAR file content error: {}".format(file_path)) sys.exit(1) diff --git a/httprunner/make.py b/httprunner/make.py index 67cc4771..4d2585af 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -5,6 +5,7 @@ from typing import Text, List, Tuple, Dict, Set, NoReturn import jinja2 from loguru import logger +from sentry_sdk import capture_exception from httprunner import exceptions, __version__ from httprunner.compat import ensure_testcase_v3_api, ensure_testcase_v3 @@ -131,6 +132,7 @@ def format_pytest_with_black(*python_paths: Text) -> NoReturn: try: subprocess.run(["black", *python_paths]) except subprocess.CalledProcessError as ex: + capture_exception(ex) logger.error(ex) diff --git a/httprunner/parser.py b/httprunner/parser.py index a43023e6..3fd9a7ad 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -3,6 +3,8 @@ import builtins import re from typing import Any, Set, Text, Callable, List, Dict +from sentry_sdk import capture_exception + from httprunner import loader, utils, exceptions from httprunner.schema import VariablesMapping, FunctionsMapping @@ -70,7 +72,8 @@ def regex_findall_variables(content: Text) -> List[Text]: for var_tuple in variable_regex_compile.findall(content): vars_list.append(var_tuple[0] or var_tuple[1]) return vars_list - except TypeError: + except TypeError as ex: + capture_exception(ex) return [] @@ -102,7 +105,8 @@ def regex_findall_functions(content: Text) -> List[Text]: """ try: return function_regex_compile.findall(content) - except TypeError: + except TypeError as ex: + capture_exception(ex) return [] diff --git a/pyproject.toml b/pyproject.toml index 7db3053d..2b239b6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "3.0.7" +version = "3.0.8" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" From 80b829f75feaa235adbe6f1107532a27efd45192 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 13:18:52 +0800 Subject: [PATCH 06/20] fix: override testsuite/testcase config verify --- httprunner/make.py | 11 +++++++---- httprunner/runner.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/httprunner/make.py b/httprunner/make.py index 4d2585af..eaa7d309 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -329,10 +329,10 @@ def make_testsuite(testsuite: Dict) -> NoReturn: # validate testsuite format load_testsuite(testsuite) - config = testsuite["config"] - testsuite_path = config["path"] + testsuite_config = testsuite["config"] + testsuite_path = testsuite_config["path"] - testsuite_variables = config.get("variables", {}) + testsuite_variables = testsuite_config.get("variables", {}) if isinstance(testsuite_variables, Text): # get variables by function, e.g. ${get_variables()} project_meta = load_project_meta(testsuite_path) @@ -360,9 +360,12 @@ def make_testsuite(testsuite: Dict) -> NoReturn: # override testcase name testcase_dict["config"]["name"] = testcase["name"] # override base_url - base_url = testsuite["config"].get("base_url") or testcase.get("base_url") + base_url = testsuite_config.get("base_url") or testcase.get("base_url") if base_url: testcase_dict["config"]["base_url"] = base_url + # override verify + if "verify" in testsuite_config: + testcase_dict["config"]["verify"] = testsuite_config["verify"] # override variables testcase_dict["config"].setdefault("variables", {}) testcase_dict["config"]["variables"].update(testcase.get("variables", {})) diff --git a/httprunner/runner.py b/httprunner/runner.py index 9b94cf5e..d3e324b1 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -101,6 +101,7 @@ class HttpRunner(object): method = parsed_request_dict.pop("method") url_path = parsed_request_dict.pop("url") url = build_url(self.__config.base_url, url_path) + parsed_request_dict["verify"] = self.__config.verify parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) # request From 8bd3904e714b9473d7adad1756ecfbca2217814d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 14:13:53 +0800 Subject: [PATCH 07/20] fix: missing request json --- docs/CHANGELOG.md | 5 +++++ httprunner/make.py | 4 ++++ httprunner/testcase.py | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4fca7a0c..05718bb4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,11 @@ - feat: add sentry sdk +**Fixed** + +- fix: missing request json +- fix: override testsuite/testcase config verify + **Changed** - change: add httprunner version in generated pytest file diff --git a/httprunner/make.py b/httprunner/make.py index eaa7d309..aaab8987 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -175,6 +175,10 @@ def make_request_chain_style(request: Dict) -> Text: data = f'"{data}"' request_chain_style += f".with_data({data})" + if "json" in request: + req_json = request["json"] + request_chain_style += f".with_json({req_json})" + if "timeout" in request: timeout = request["timeout"] request_chain_style += f".set_timeout({timeout})" diff --git a/httprunner/testcase.py b/httprunner/testcase.py index bcbfb07d..25572275 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -215,6 +215,10 @@ class RequestWithOptionalArgs(object): self.__t_step.request.data = data return self + def with_json(self, req_json) -> "RequestWithOptionalArgs": + self.__t_step.request.req_json = req_json + return self + def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": self.__t_step.request.timeout = timeout return self From 8ca177cc44c7dffff1799e3c1a9e9ddb296e4cd8 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 14:34:41 +0800 Subject: [PATCH 08/20] change: log current working directory when debugtalk.py not found --- httprunner/loader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/httprunner/loader.py b/httprunner/loader.py index 4ef030e3..821c9204 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -290,8 +290,11 @@ def locate_file(start_path: Text, file_name: Text) -> Text: return os.path.abspath(file_path) # current working directory - if os.path.abspath(start_dir_path) == os.getcwd(): - raise exceptions.FileNotFound(f"{file_name} not found in {start_path}") + cwd = os.getcwd() + if os.path.abspath(start_dir_path) == cwd: + raise exceptions.FileNotFound( + f"{file_name} not found for {start_path}\ncurrent working directory: {cwd}" + ) # system root dir # Windows, e.g. 'E:\\' From 75228d15f0b21f9c6ac6ddf75956ae93fe3ed107 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 15:15:46 +0800 Subject: [PATCH 09/20] fix: only strip whitespaces and tabs, \n\r are left because they maybe used in changeset --- docs/CHANGELOG.md | 1 + httprunner/parser.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 05718bb4..78b5235d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,7 @@ - fix: missing request json - fix: override testsuite/testcase config verify +- fix: only strip whitespaces and tabs, \n\r are left because they maybe used in changeset **Changed** diff --git a/httprunner/parser.py b/httprunner/parser.py index 3fd9a7ad..c36363ee 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -362,7 +362,8 @@ def parse_data( # content in string format may contains variables and functions variables_mapping = variables_mapping or {} functions_mapping = functions_mapping or {} - raw_data = raw_data.strip() + # only strip whitespaces and tabs, \n\r is left because they maybe used in changeset + raw_data = raw_data.strip(" \t") return parse_string(raw_data, variables_mapping, functions_mapping) elif isinstance(raw_data, (list, set, tuple)): From 785610f735955f0ca6e6b89be1cde54167b6c323 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 15:30:24 +0800 Subject: [PATCH 10/20] fix: try json loads request_body only if it is not None --- httprunner/client.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/httprunner/client.py b/httprunner/client.py index d3cace95..d396917e 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -43,20 +43,21 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: # record actual request info request_headers = dict(resp_obj.request.headers) request_cookies = resp_obj.request._cookies.get_dict() - request_body = resp_obj.request.body - try: - request_body = json.loads(request_body) - except json.JSONDecodeError as ex: - # str: Unexpected UTF-8 BOM (decode using utf-8-sig) - capture_exception(ex) - except UnicodeDecodeError as ex: - # bytes/bytearray: request body in protobuf - capture_exception(ex) - except TypeError as ex: - # neither str nor bytes/bytearray, e.g. None - capture_exception(ex) - if request_body: + request_body = resp_obj.request.body + if request_body is not None: + try: + request_body = json.loads(request_body) + except json.JSONDecodeError as ex: + # str: Unexpected UTF-8 BOM (decode using utf-8-sig) + capture_exception(ex) + except UnicodeDecodeError as ex: + # bytes/bytearray: request body in protobuf + capture_exception(ex) + except TypeError as ex: + # neither str nor bytes/bytearray + capture_exception(ex) + request_content_type = lower_dict_keys(request_headers).get("content-type") if request_content_type and "multipart/form-data" in request_content_type: # upload file type From 5d9e87d096a6e80072934f12b8f7f15f5d083128 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 15:42:12 +0800 Subject: [PATCH 11/20] change: do not capture exception when request body is string --- httprunner/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httprunner/client.py b/httprunner/client.py index d396917e..0923d16e 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -49,8 +49,8 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: try: request_body = json.loads(request_body) except json.JSONDecodeError as ex: - # str: Unexpected UTF-8 BOM (decode using utf-8-sig) - capture_exception(ex) + # str: a=1&b=2 + pass except UnicodeDecodeError as ex: # bytes/bytearray: request body in protobuf capture_exception(ex) From 9e7a2687b5200e4bf3eff040caacafeb46400092 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 15:55:09 +0800 Subject: [PATCH 12/20] change: replace raise with exit 1 --- httprunner/compat.py | 13 +++++++------ tests/compat_test.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/httprunner/compat.py b/httprunner/compat.py index 2d7fd593..b0b0e740 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -2,11 +2,11 @@ This module handles compatibility issues between testcase format v2 and v3. """ import os +import sys from typing import List, Dict, Text, Union from loguru import logger -from httprunner import exceptions from httprunner.loader import load_project_meta from httprunner.utils import sort_dict_by_custom_order @@ -28,9 +28,8 @@ def convert_jmespath(raw: Text) -> Text: elif item.isdigit(): # convert lst.0.name to lst[0].name if len(raw_list) == 0: - raise exceptions.FileFormatError( - f"Invalid jmespath: {raw}, jmespath should startswith headers/body/status_code/cookies" - ) + logger.error(f"Invalid jmespath: {raw}") + sys.exit(1) last_item = raw_list.pop() item = f"{last_item}[{item}]" @@ -60,7 +59,8 @@ def convert_extractors(extractors: Union[List, Dict]) -> Dict: elif isinstance(extractors, Dict): v3_extractors = extractors else: - raise exceptions.FileFormatError(f"Invalid extractor: {extractors}") + logger.error(f"Invalid extractor: {extractors}") + sys.exit(1) for k, v in v3_extractors.items(): v3_extractors[k] = convert_jmespath(v) @@ -207,7 +207,8 @@ def generate_conftest_for_summary(args: List): # FIXME: several test paths maybe specified break else: - raise exceptions.FileNotFound(f"No test path specified!") + logger.error(f"No valid test path specified! \nargs: {args}") + sys.exit(1) project_meta = load_project_meta(test_path) conftest_path = os.path.join(project_meta.PWD, "conftest.py") diff --git a/tests/compat_test.py b/tests/compat_test.py index c44cd2a7..1b2dba3e 100644 --- a/tests/compat_test.py +++ b/tests/compat_test.py @@ -1,7 +1,7 @@ import os import unittest -from httprunner import compat, exceptions +from httprunner import compat class TestCompat(unittest.TestCase): @@ -19,7 +19,7 @@ class TestCompat(unittest.TestCase): compat.convert_jmespath("body.data.buildings.0.building_id"), "body.data.buildings[0].building_id", ) - with self.assertRaises(exceptions.FileFormatError): + with self.assertRaises(SystemExit): compat.convert_jmespath("2.buildings.0.building_id") def test_convert_extractors(self): From e5383d683594a64a947f71fe895a3eb1b2fd4649 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 16:06:02 +0800 Subject: [PATCH 13/20] docs: update examples --- examples/httpbin/basic_test.py | 2 +- examples/httpbin/hooks_test.py | 2 +- examples/httpbin/load_image_test.py | 2 +- examples/httpbin/upload_test.py | 2 +- examples/httpbin/validate_test.py | 2 +- .../demo_testsuite_yml/request_with_functions_test.py | 2 +- .../demo_testsuite_yml/request_with_testcase_reference_test.py | 2 +- examples/postman_echo/request_methods/hardcode_test.py | 2 +- .../postman_echo/request_methods/request_with_functions_test.py | 2 +- .../request_methods/request_with_testcase_reference_test.py | 2 +- .../postman_echo/request_methods/request_with_variables_test.py | 2 +- .../request_methods/validate_with_functions_test.py | 2 +- .../request_methods/validate_with_variables_test.py | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index d09a946b..24cd405e 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/httpbin/basic.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index 7b917908..0e2cd0b9 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/httpbin/hooks.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index c59e7274..36453b6c 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/httpbin/load_image.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index 08b22543..53ce4461 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/httpbin/upload.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index 018406fb..c9115e60 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/httpbin/validate.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py index 2057e7b7..8164a197 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py index 85dd7423..93e7297d 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py index 9fbfcd9f..09fa0cfd 100644 --- a/examples/postman_echo/request_methods/hardcode_test.py +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/hardcode.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py index 478bf7b3..cf8a91b3 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/request_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py index 2c606542..b015e9c7 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml import os diff --git a/examples/postman_echo/request_methods/request_with_variables_test.py b/examples/postman_echo/request_methods/request_with_variables_test.py index dcbcf46d..ecfa1586 100644 --- a/examples/postman_echo/request_methods/request_with_variables_test.py +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/request_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/validate_with_functions_test.py b/examples/postman_echo/request_methods/validate_with_functions_test.py index f4495555..4378c92b 100644 --- a/examples/postman_echo/request_methods/validate_with_functions_test.py +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/validate_with_functions.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase diff --git a/examples/postman_echo/request_methods/validate_with_variables_test.py b/examples/postman_echo/request_methods/validate_with_variables_test.py index d0db20af..3c3c0291 100644 --- a/examples/postman_echo/request_methods/validate_with_variables_test.py +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -1,4 +1,4 @@ -# NOTICE: Generated By HttpRunner v3.0.7 +# NOTICE: Generated By HttpRunner v3.0.8 # FROM: examples/postman_echo/request_methods/validate_with_variables.yml from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase From 7399fa27f571d0658cf7ffcf860318626cc5b56c Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 16:28:11 +0800 Subject: [PATCH 14/20] change: update capture exception --- httprunner/client.py | 4 ++-- httprunner/ext/har2case/core.py | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/httprunner/client.py b/httprunner/client.py index 0923d16e..01f10e0a 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -48,14 +48,14 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: if request_body is not None: try: request_body = json.loads(request_body) - except json.JSONDecodeError as ex: + except json.JSONDecodeError: # str: a=1&b=2 pass except UnicodeDecodeError as ex: # bytes/bytearray: request body in protobuf capture_exception(ex) except TypeError as ex: - # neither str nor bytes/bytearray + # neither str nor bytes/bytearray, e.g. capture_exception(ex) request_content_type = lower_dict_keys(request_headers).get("content-type") diff --git a/httprunner/ext/har2case/core.py b/httprunner/ext/har2case/core.py index 5834c696..3c491cf5 100644 --- a/httprunner/ext/har2case/core.py +++ b/httprunner/ext/har2case/core.py @@ -167,8 +167,8 @@ class HarParser(object): try: post_data = json.loads(post_data) request_data_key = "json" - except JSONDecodeError as ex: - capture_exception(ex) + except JSONDecodeError: + pass elif mimeType.startswith("application/x-www-form-urlencoded"): post_data = utils.convert_x_www_form_urlencoded_to_dict(post_data) else: @@ -238,16 +238,12 @@ class HarParser(object): try: resp_content_json = json.loads(content) - except JSONDecodeError as ex: - capture_exception(ex) - logger.warning( - "response content can not be loaded as json: {}".format( - content.encode("utf-8") - ) - ) + except JSONDecodeError: + logger.warning(f"response content can not be loaded as json: {content}") return if not isinstance(resp_content_json, dict): + # e.g. ['a', 'b'] return for key, value in resp_content_json.items(): From 22c27ec15a7edeee300cbef0e754f631d4e2c3f7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 16:40:35 +0800 Subject: [PATCH 15/20] docs: update changelog --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 78b5235d..d166e67c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 3.0.7 (2020-06-04) +## 3.0.8 (2020-06-04) **Added** From 30f256fc0f883f9d0933b15cc2a4882ec4558fa3 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 17:02:09 +0800 Subject: [PATCH 16/20] change: rename to StepRequestExtraction --- httprunner/testcase.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 25572275..67c46c6f 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -172,11 +172,11 @@ class StepValidation(object): return self.__t_step -class StepExtraction(object): +class StepRequestExtraction(object): def __init__(self, step: TStep): self.__t_step = step - def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepExtraction": + def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction": self.__t_step.extract[var_name] = jmes_path return self @@ -238,8 +238,8 @@ class RequestWithOptionalArgs(object): # def hooks(self): # pass - def extract(self) -> StepExtraction: - return StepExtraction(self.__t_step) + def extract(self) -> StepRequestExtraction: + return StepRequestExtraction(self.__t_step) def validate(self) -> StepValidation: return StepValidation(self.__t_step) @@ -305,7 +305,7 @@ class Step(object): def __init__( self, step: Union[ - StepValidation, StepExtraction, RequestWithOptionalArgs, RunTestCase + StepValidation, StepRequestExtraction, RequestWithOptionalArgs, RunTestCase ], ): self.__t_step = step.perform() From 5188f7950a38b41774038a0e4af8ec52edc33344 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 17:04:36 +0800 Subject: [PATCH 17/20] change: rename to StepRequestValidation --- httprunner/testcase.py | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 67c46c6f..daaedc7b 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -50,35 +50,35 @@ class Config(object): ) -class StepValidation(object): +class StepRequestValidation(object): def __init__(self, step: TStep): self.__t_step = step - def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepValidation": + def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepRequestValidation": self.__t_step.validators.append({"equal": [jmes_path, expected_value]}) return self def assert_not_equal( self, jmes_path: Text, expected_value: Any - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"not_equal": [jmes_path, expected_value]}) return self def assert_greater_than( self, jmes_path: Text, expected_value: Union[int, float] - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"greater_than": [jmes_path, expected_value]}) return self def assert_less_than( self, jmes_path: Text, expected_value: Union[int, float] - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"less_than": [jmes_path, expected_value]}) return self def assert_greater_or_equals( self, jmes_path: Text, expected_value: Union[int, float] - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append( {"greater_or_equals": [jmes_path, expected_value]} ) @@ -86,19 +86,19 @@ class StepValidation(object): def assert_less_or_equals( self, jmes_path: Text, expected_value: Union[int, float] - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"less_or_equals": [jmes_path, expected_value]}) return self def assert_length_equal( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"length_equal": [jmes_path, expected_value]}) return self def assert_length_greater_than( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append( {"length_greater_than": [jmes_path, expected_value]} ) @@ -106,7 +106,7 @@ class StepValidation(object): def assert_length_less_than( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append( {"length_less_than": [jmes_path, expected_value]} ) @@ -114,7 +114,7 @@ class StepValidation(object): def assert_length_greater_or_equals( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append( {"length_greater_or_equals": [jmes_path, expected_value]} ) @@ -122,7 +122,7 @@ class StepValidation(object): def assert_length_less_or_equals( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append( {"length_less_or_equals": [jmes_path, expected_value]} ) @@ -130,41 +130,41 @@ class StepValidation(object): def assert_string_equals( self, jmes_path: Text, expected_value: int - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"string_equals": [jmes_path, expected_value]}) return self def assert_startswith( self, jmes_path: Text, expected_value: Text - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"startswith": [jmes_path, expected_value]}) return self def assert_endswith( self, jmes_path: Text, expected_value: Text - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"endswith": [jmes_path, expected_value]}) return self def assert_regex_match( self, jmes_path: Text, expected_value: Text - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"regex_match": [jmes_path, expected_value]}) return self - def assert_contains(self, jmes_path: Text, expected_value: Any) -> "StepValidation": + def assert_contains(self, jmes_path: Text, expected_value: Any) -> "StepRequestValidation": self.__t_step.validators.append({"contains": [jmes_path, expected_value]}) return self def assert_contained_by( self, jmes_path: Text, expected_value: Any - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"contained_by": [jmes_path, expected_value]}) return self def assert_type_match( self, jmes_path: Text, expected_value: Text - ) -> "StepValidation": + ) -> "StepRequestValidation": self.__t_step.validators.append({"type_match": [jmes_path, expected_value]}) return self @@ -188,8 +188,8 @@ class StepRequestExtraction(object): # # TODO: extract response json with jsonpath # pass - def validate(self) -> StepValidation: - return StepValidation(self.__t_step) + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__t_step) def perform(self) -> TStep: return self.__t_step @@ -241,8 +241,8 @@ class RequestWithOptionalArgs(object): def extract(self) -> StepRequestExtraction: return StepRequestExtraction(self.__t_step) - def validate(self) -> StepValidation: - return StepValidation(self.__t_step) + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__t_step) def perform(self) -> TStep: return self.__t_step @@ -305,7 +305,7 @@ class Step(object): def __init__( self, step: Union[ - StepValidation, StepRequestExtraction, RequestWithOptionalArgs, RunTestCase + StepRequestValidation, StepRequestExtraction, RequestWithOptionalArgs, RunTestCase ], ): self.__t_step = step.perform() From 0fdb9d426af01979bdb996220a10685adef81dda Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 17:38:18 +0800 Subject: [PATCH 18/20] feat: log export variables --- httprunner/runner.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/httprunner/runner.py b/httprunner/runner.py index d3e324b1..cbac56b9 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -46,6 +46,7 @@ class HttpRunner(object): __step_datas: List[StepData] = None __session: HttpSession = None __session_variables: VariablesMapping = {} + __export_variables: VariablesMapping = {} # time __start_at: float = 0 __duration: float = 0 @@ -249,6 +250,7 @@ class HttpRunner(object): self.__step_datas: List[StepData] = [] self.__session = self.__session or HttpSession() self.__session_variables = {} + self.__export_variables = {} # run teststeps for step in self.__teststeps: @@ -270,6 +272,11 @@ class HttpRunner(object): self.__session_variables.update(extract_mapping) self.__duration = time.time() - self.__start_at + + self.__export_variables = self.get_export_variables() + if self.__export_variables: + logger.info(f"export variables: {self.__export_variables}") + return self def run_path(self, path: Text) -> "HttpRunner": @@ -294,6 +301,9 @@ class HttpRunner(object): return self.__step_datas def get_export_variables(self) -> Dict: + if self.__export_variables: + return self.__export_variables + export_vars_mapping = {} for var_name in self.__config.export: if var_name not in self.__session_variables: From 81d6f2d65bcf8b02c562392464bfe504b703854e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 18:20:55 +0800 Subject: [PATCH 19/20] feat: extract session variable from referenced testcase step --- docs/CHANGELOG.md | 1 + .../request_with_functions_test.py | 1 + .../request_with_testcase_reference_test.py | 17 +++++++++ .../request_with_functions.yml | 1 + .../request_with_functions_test.py | 1 + .../request_with_testcase_reference.yml | 17 +++++++++ .../request_with_testcase_reference_test.py | 17 +++++++++ httprunner/compat.py | 8 +++- httprunner/make.py | 22 ++++++++--- httprunner/schema.py | 2 +- httprunner/testcase.py | 37 ++++++++++++++++--- tests/runner_test.py | 2 +- 12 files changed, 111 insertions(+), 15 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d166e67c..d6a0b667 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ **Added** - feat: add sentry sdk +- feat: extract session variable from referenced testcase step **Fixed** diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py index 8164a197..345887c2 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py @@ -10,6 +10,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .variables(**{"foo1": "session_bar1", "var1": "testsuite_val1"}) .base_url("https://postman-echo.com") .verify(False) + .export(*["session_foo2"]) ) teststeps = [ diff --git a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py index 93e7297d..ca0232b9 100644 --- a/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py @@ -26,6 +26,23 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): RunTestCase("request with functions") .with_variables(**{"foo1": "override_bar1"}) .call(RequestWithFunctions) + .extract(*["session_foo2"]) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "session_bar2") ), ] diff --git a/examples/postman_echo/request_methods/request_with_functions.yml b/examples/postman_echo/request_methods/request_with_functions.yml index 6fc68325..33952cab 100644 --- a/examples/postman_echo/request_methods/request_with_functions.yml +++ b/examples/postman_echo/request_methods/request_with_functions.yml @@ -4,6 +4,7 @@ config: foo1: session_bar1 base_url: "https://postman-echo.com" verify: False + export: ["session_foo2"] teststeps: - diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py index cf8a91b3..99a103e9 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -10,6 +10,7 @@ class TestCaseRequestWithFunctions(HttpRunner): .variables(**{"foo1": "session_bar1"}) .base_url("https://postman-echo.com") .verify(False) + .export(*["session_foo2"]) ) teststeps = [ diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference.yml b/examples/postman_echo/request_methods/request_with_testcase_reference.yml index 3b2bfbdc..85138047 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference.yml +++ b/examples/postman_echo/request_methods/request_with_testcase_reference.yml @@ -11,3 +11,20 @@ teststeps: variables: foo1: override_bar1 testcase: request_methods/request_with_functions.yml + extract: + - session_foo2 +- + name: post form data + variables: + foo1: bar1 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$session_foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "session_bar1"] + - eq: ["body.form.foo2", "session_bar2"] diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py index b015e9c7..24a5d107 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -26,6 +26,23 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): RunTestCase("request with functions") .with_variables(**{"foo1": "override_bar1"}) .call(RequestWithFunctions) + .extract(*["session_foo2"]) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "session_bar1") + .assert_equal("body.form.foo2", "session_bar2") ), ] diff --git a/httprunner/compat.py b/httprunner/compat.py index b0b0e740..54addff2 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -133,7 +133,10 @@ def ensure_step_attachment(step: Dict) -> Dict: test_dict["teardown_hooks"] = step["teardown_hooks"] if "extract" in step: - test_dict["extract"] = convert_extractors(step["extract"]) + if step.get("request"): + test_dict["extract"] = convert_extractors(step["extract"]) + elif step.get("testcase"): + test_dict["extract"] = step["extract"] if "validate" in step: test_dict["validate"] = convert_validators(step["validate"]) @@ -164,6 +167,8 @@ def ensure_testcase_v3(test_content: Dict) -> Dict: for step in test_content["teststeps"]: teststep = {} + teststep.update(ensure_step_attachment(step)) + if "request" in step: teststep["request"] = step.pop("request") elif "api" in step: @@ -171,7 +176,6 @@ def ensure_testcase_v3(test_content: Dict) -> Dict: elif "testcase" in step: teststep["testcase"] = step.pop("testcase") - teststep.update(ensure_step_attachment(step)) teststep = sort_step_by_custom_order(teststep) v3_content["teststeps"].append(teststep) diff --git a/httprunner/make.py b/httprunner/make.py index aaab8987..66fd9a1b 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -149,6 +149,9 @@ def make_config_chain_style(config: Dict) -> Text: if "verify" in config: config_chain_style += f'.verify({config["verify"]})' + if "export" in config: + config_chain_style += f'.export(*{config["export"]})' + return config_chain_style @@ -204,7 +207,7 @@ def make_teststep_chain_style(teststep: Dict) -> Text: elif teststep.get("testcase"): step_info = f'RunTestCase("{teststep["name"]}")' else: - raise exceptions.TestCaseFormatError + raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}") if "variables" in teststep: variables = teststep["variables"] @@ -217,11 +220,18 @@ def make_teststep_chain_style(teststep: Dict) -> Text: call_ref_testcase = f".call({testcase})" step_info += call_ref_testcase - if "extract" in teststep: - step_info += ".extract()" - - for extract_name, extract_path in teststep["extract"].items(): - step_info += f'.with_jmespath("{extract_path}", "{extract_name}")' + extract_info = teststep.get("extract") + if extract_info: + if isinstance(extract_info, Dict): + # request step + step_info += ".extract()" + for extract_name, extract_path in extract_info.items(): + step_info += f'.with_jmespath("{extract_path}", "{extract_name}")' + elif isinstance(extract_info, List): + # reference testcase step + step_info += f".extract(*{extract_info})" + else: + raise exceptions.TestCaseFormatError(f"Invalid extract: {extract_info}") if "validate" in teststep: step_info += ".validate()" diff --git a/httprunner/schema.py b/httprunner/schema.py index 59ff17f6..be60bf41 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -66,7 +66,7 @@ class TStep(BaseModel): variables: VariablesMapping = {} setup_hooks: Hook = [] teardown_hooks: Hook = [] - extract: Dict[Text, Text] = {} + extract: Union[Dict[Text, Text], List[Text]] = {} validators: Validators = Field([], alias="validate") validate_script: List[Text] = [] diff --git a/httprunner/testcase.py b/httprunner/testcase.py index daaedc7b..fc738851 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -16,6 +16,7 @@ class Config(object): self.__variables = {} self.__base_url = "" self.__verify = False + self.__export = [] caller_frame = inspect.stack()[1] self.__path = caller_frame.filename @@ -40,12 +41,17 @@ class Config(object): self.__verify = verify return self + def export(self, *export_var_name: Text) -> "Config": + self.__export.extend(export_var_name) + return self + def perform(self) -> TConfig: return TConfig( name=self.__name, base_url=self.__base_url, verify=self.__verify, variables=self.__variables, + export=list(set(self.__export)), path=self.__path, ) @@ -54,7 +60,9 @@ class StepRequestValidation(object): def __init__(self, step: TStep): self.__t_step = step - def assert_equal(self, jmes_path: Text, expected_value: Any) -> "StepRequestValidation": + def assert_equal( + self, jmes_path: Text, expected_value: Any + ) -> "StepRequestValidation": self.__t_step.validators.append({"equal": [jmes_path, expected_value]}) return self @@ -152,7 +160,9 @@ class StepRequestValidation(object): self.__t_step.validators.append({"regex_match": [jmes_path, expected_value]}) return self - def assert_contains(self, jmes_path: Text, expected_value: Any) -> "StepRequestValidation": + def assert_contains( + self, jmes_path: Text, expected_value: Any + ) -> "StepRequestValidation": self.__t_step.validators.append({"contains": [jmes_path, expected_value]}) return self @@ -285,6 +295,19 @@ class RunRequest(object): return RequestWithOptionalArgs(self.__t_step) +class StepRefCase(object): + def __init__(self, step: TStep): + self.__t_step = step + self.__t_step.extract = [] + + def extract(self, *var_name: Text) -> "StepRefCase": + self.__t_step.extract.extend(var_name) + return self + + def perform(self) -> TStep: + return self.__t_step + + class RunTestCase(object): def __init__(self, name: Text): self.__t_step = TStep(name=name) @@ -293,9 +316,9 @@ class RunTestCase(object): self.__t_step.variables.update(variables) return self - def call(self, testcase: Callable): + def call(self, testcase: Callable) -> StepRefCase: self.__t_step.testcase = testcase - return self + return StepRefCase(self.__t_step) def perform(self) -> TStep: return self.__t_step @@ -305,7 +328,11 @@ class Step(object): def __init__( self, step: Union[ - StepRequestValidation, StepRequestExtraction, RequestWithOptionalArgs, RunTestCase + StepRequestValidation, + StepRequestExtraction, + RequestWithOptionalArgs, + RunTestCase, + StepRefCase, ], ): self.__t_step = step.perform() diff --git a/tests/runner_test.py b/tests/runner_test.py index 39d9d978..f0dc4a87 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -25,4 +25,4 @@ class TestHttpRunner(unittest.TestCase): self.assertTrue(result.success) self.assertEqual(result.name, "request methods testcase: reference testcase") self.assertEqual(result.step_datas[0].name, "request with functions") - self.assertEqual(len(result.step_datas), 1) + self.assertEqual(len(result.step_datas), 2) From 9796c0b8a6ee3b655a225dc036238a8e9c57737a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 4 Jun 2020 18:36:37 +0800 Subject: [PATCH 20/20] fix: log testcase duration before raise ValidationFailure --- docs/CHANGELOG.md | 1 + httprunner/runner.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d6a0b667..c9518eef 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,7 @@ - fix: missing request json - fix: override testsuite/testcase config verify - fix: only strip whitespaces and tabs, \n\r are left because they maybe used in changeset +- fix: log testcase duration before raise ValidationFailure **Changed** diff --git a/httprunner/runner.py b/httprunner/runner.py index cbac56b9..b90886ea 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -149,6 +149,8 @@ class HttpRunner(object): except ValidationFailure: self.__session.data.success = False log_req_resp_details() + # log testcase duration before raise ValidationFailure + self.__duration = time.time() - self.__start_at raise finally: # save request & response meta data