From 80a600bcccbfa4ee8a74eb84bd083465423d514d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 15 May 2020 18:30:06 +0800 Subject: [PATCH] feat: run testcase with pytest --- httprunner/cli.py | 158 ++++++++++++---------------- httprunner/ext/make/__init__.py | 18 ++++ poetry.lock | 178 +++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 260 insertions(+), 95 deletions(-) diff --git a/httprunner/cli.py b/httprunner/cli.py index 9dfee0e8..23c7f1b4 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -18,73 +18,43 @@ $ pip install locustio print(msg) sys.exit(1) -from loguru import logger +import pytest -from httprunner import __description__, __version__ -from httprunner.api import HttpRunner +from httprunner import __description__, __version__, exceptions from httprunner.ext.har2case import init_har2case_parser, main_har2case from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold from httprunner.ext.locusts import init_parser_locusts, main_locusts -from httprunner.ext.make import init_make_parser, main_make +from httprunner.ext.make import init_make_parser, main_make, convert_testcase_path def init_parser_run(subparsers): - sub_parser_run = subparsers.add_parser("run", help="Run HttpRunner testcases.") - - sub_parser_run.add_argument( - "testfile_paths", - nargs="*", - help="Specify api/testcase/testsuite file paths to run.", + sub_parser_run = subparsers.add_parser( + "run", help="Make HttpRunner testcases and run with pytest." ) - sub_parser_run.add_argument( - "--log-level", default="INFO", help="Specify logging level, default is INFO." - ) - sub_parser_run.add_argument("--log-file", help="Write logs to specified file path.") - sub_parser_run.add_argument( - "--dot-env-path", - help="Specify .env file path, which is useful for keeping sensitive data.", - ) - sub_parser_run.add_argument( - "--report-template", help="Specify report template path." - ) - sub_parser_run.add_argument("--report-dir", help="Specify report save directory.") - sub_parser_run.add_argument( - "--report-file", - help="Specify report file path, this has higher priority than specifying report dir.", - ) - sub_parser_run.add_argument( - "--save-tests", - action="store_true", - default=False, - help="Save loaded/parsed/vars_out/summary json data to JSON files.", - ) - return sub_parser_run -def main_run(args): - runner = HttpRunner( - save_tests=args.save_tests, log_level=args.log_level, log_file=args.log_file - ) +def main_run(extra_args): + tests_path_list = [] + for index, item in enumerate(extra_args): + if not os.path.exists(item): + # item is not file/folder path + continue + elif os.path.isfile(item): + # replace YAML/JSON file path with generated python file + extra_args[index] = convert_testcase_path(item) - err_code = 0 - try: - for path in args.testfile_paths: - testsuite_summary = runner.run_path(path, dot_env_path=args.dot_env_path) - report_dir = args.report_dir or os.path.join(os.getcwd(), "reports") - runner.gen_html_report( - report_template=args.report_template, - report_dir=report_dir, - report_file=args.report_file, - ) - err_code |= 0 if testsuite_summary and testsuite_summary.success else 1 - except Exception as ex: - logger.error( - f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!\n{str(ex)}" - ) - err_code = 1 + tests_path_list.append(item) - sys.exit(err_code) + if len(tests_path_list) == 0: + # has not specified any testcase path + raise exceptions.ParamsError("Missed testcase path") + + main_make(tests_path_list) + + if "-s" not in extra_args: + extra_args.insert(0, "-s") + pytest.main(extra_args) def main(): @@ -102,8 +72,35 @@ def main(): sub_parser_locusts = init_parser_locusts(subparsers) sub_parser_make = init_make_parser(subparsers) + if len(sys.argv) == 1: + # httprunner + parser.print_help() + sys.exit(0) + elif len(sys.argv) == 2: + # print help for sub-commands + if sys.argv[1] == "startproject": + # httprunner startproject + sub_parser_scaffold.print_help() + elif sys.argv[1] == "har2case": + # httprunner har2case + sub_parser_har2case.print_help() + elif sys.argv[1] == "locusts": + # httprunner locusts + sub_parser_locusts.print_help() + elif sys.argv[1] == "run": + # httprunner run + pytest.main(["-h"]) + elif sys.argv[1] == "make": + # httprunner make + sub_parser_make.print_help() + sys.exit(0) + elif len(sys.argv) == 3 and sys.argv[1] == "run" and sys.argv[2] in ["-h", "--help"]: + # httprunner run -h + pytest.main(["-h"]) + sys.exit(0) + extra_args = [] - if len(sys.argv) >= 2 and sys.argv[1] == "locusts": + if len(sys.argv) >= 2 and sys.argv[1] in ["run", "locusts"]: args, extra_args = parser.parse_known_args() else: args = parser.parse_args() @@ -112,49 +109,15 @@ def main(): print(f"{__version__}") sys.exit(0) - if len(sys.argv) == 1: - # httprunner - parser.print_help() - sys.exit(0) - - elif sys.argv[1] == "run": - # httprunner run - if len(sys.argv) == 2: - sub_parser_run.print_help() - sys.exit(0) - - main_run(args) - + if sys.argv[1] == "run": + main_run(extra_args) elif sys.argv[1] == "startproject": - # httprunner startproject - if len(sys.argv) == 2: - sub_parser_scaffold.print_help() - sys.exit(0) - main_scaffold(args) - elif sys.argv[1] == "har2case": - # httprunner har2case - if len(sys.argv) == 2: - sub_parser_har2case.print_help() - sys.exit(0) - main_har2case(args) - elif sys.argv[1] == "locusts": - # httprunner locusts - if len(sys.argv) == 2: - sub_parser_locusts.print_help() - sys.exit(0) - main_locusts(args, extra_args) - elif sys.argv[1] == "make": - # httprunner make - if len(sys.argv) == 2: - sub_parser_make.print_help() - sys.exit(0) - main_make(args.testcase_path) @@ -162,9 +125,16 @@ def main_hrun_alias(): """ command alias hrun = httprunner run """ - if len(sys.argv) == 2 and sys.argv[1] in ["-V", "--version"]: - # hrun -V - sys.argv = ["httprunner", "-V"] + if len(sys.argv) == 2: + if sys.argv[1] in ["-V", "--version"]: + # hrun -V + sys.argv = ["httprunner", "-V"] + elif sys.argv[1] in ["-h", "--help"]: + pytest.main(["-h"]) + sys.exit(0) + else: + # hrun /path/to/testcase + sys.argv.insert(1, "run") else: sys.argv.insert(1, "run") diff --git a/httprunner/ext/make/__init__.py b/httprunner/ext/make/__init__.py index 91a10356..70456cd7 100644 --- a/httprunner/ext/make/__init__.py +++ b/httprunner/ext/make/__init__.py @@ -60,8 +60,26 @@ def make_testcase(testcase_path: str) -> Union[str, None]: return testcase_python_path +def convert_testcase_path(testcase_path: Text) -> Text: + """convert single YAML/JSON testcase path to python file""" + if os.path.isdir(testcase_path): + # folder does not need to convert + return testcase_path + + file_suffix = os.path.splitext(testcase_path)[1].lower() + if file_suffix == ".json": + return testcase_path.replace(".json", "_test.py") + elif file_suffix == ".yaml": + return testcase_path.replace(".yaml", "_test.py") + elif file_suffix == ".yml": + return testcase_path.replace(".yml", "_test.py") + else: + raise exceptions.ParamsError("") + + def format_with_black(tests_path: Text): logger.info("format testcases with black ...") + tests_path = convert_testcase_path(tests_path) try: subprocess.run(["black", tests_path]) except subprocess.CalledProcessError as ex: diff --git a/poetry.lock b/poetry.lock index 03058613..41bdcda9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,15 @@ optional = false python-versions = "*" version = "1.4.4" +[[package]] +category = "main" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + [[package]] category = "main" description = "Classes Without Boilerplate" @@ -193,6 +202,22 @@ optional = false python-versions = "*" version = "0.11" +[[package]] +category = "main" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + [[package]] category = "dev" description = "Various helpers to pass data to untrusted environments and back." @@ -250,6 +275,26 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "1.1.1" +[[package]] +category = "main" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.2.0" + +[[package]] +category = "main" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.3" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + [[package]] category = "main" description = "Utility library for gitignore style pattern matching of file paths." @@ -258,6 +303,30 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.8.0" +[[package]] +category = "main" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "main" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.1" + [[package]] category = "main" description = "Data validation and settings management using python 3.6 type hinting" @@ -276,6 +345,40 @@ dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] typing_extensions = ["typing-extensions (>=3.7.2)"] +[[package]] +category = "main" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "main" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.4.2" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa-mypy = ["mypy (v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + [[package]] category = "main" description = "YAML parser and emitter for Python" @@ -321,6 +424,14 @@ version = "0.9.1" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" + [[package]] category = "dev" description = "The little ASGI library that shines." @@ -385,6 +496,14 @@ optional = false python-versions = "*" version = "0.14.0" +[[package]] +category = "main" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.9" + [[package]] category = "dev" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" @@ -418,8 +537,21 @@ version = "1.0.1" [package.extras] dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] +[[package]] +category = "main" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + [metadata] -content-hash = "326939a3d6944742cff4c4a14e14e9cf3e6fc032b0fd5ee9d44604e9aee45ecd" +content-hash = "e0fe5a044fc942eba590b0a3778679bc09bbec8703f1abdda58e2f76bc99ee49" python-versions = "^3.6" [metadata.files] @@ -431,6 +563,10 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, @@ -548,6 +684,10 @@ immutables = [ {file = "immutables-0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:6b6d8d035e5888baad3db61dfb167476838a63afccecd927c365f228bb55754c"}, {file = "immutables-0.11.tar.gz", hash = "sha256:d6850578a0dc6530ac19113cfe4ddc13903df635212d498f176fe601a8a5a4a3"}, ] +importlib-metadata = [ + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, +] itsdangerous = [ {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, @@ -594,10 +734,26 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] +more-itertools = [ + {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, + {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, +] +packaging = [ + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, +] pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, +] pydantic = [ {file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"}, {file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"}, @@ -614,6 +770,14 @@ pydantic = [ {file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"}, {file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"}, + {file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"}, +] pyyaml = [ {file = "PyYAML-5.2-cp27-cp27m-win32.whl", hash = "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc"}, {file = "PyYAML-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"}, @@ -658,6 +822,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"}, ] +six = [ + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] starlette = [ {file = "starlette-0.12.9.tar.gz", hash = "sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411"}, ] @@ -707,6 +875,10 @@ uvloop = [ {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, ] +wcwidth = [ + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, +] websockets = [ {file = "websockets-8.0.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e906128532a14b9d264a43eb48f9b3080d53a9bda819ab45bf56b8039dc606ac"}, {file = "websockets-8.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:83e63aa73331b9ca21af61df8f115fb5fbcba3f281bee650a4ad16a40cd1ef15"}, @@ -728,3 +900,7 @@ win32-setctime = [ {file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"}, {file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"}, ] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyproject.toml b/pyproject.toml index 581e6588..dd012dfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ pydantic = "^1.4" loguru = "^0.4.1" jmespath = "^0.9.5" black = "^19.10b0" +pytest = "^5.4.2" [tool.poetry.dev-dependencies] flask = "<1.0.0"