diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index da6cc0be..6064459e 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -30,14 +30,13 @@ jobs: poetry build ls dist/*.whl | xargs pip install # test installation hrun -V - hrun run -h - hrun startproject -h - hrun har2case -h - pip install locustio - hrun locusts -h + har2case -h + httprunner run -h + httprunner startproject -h + httprunner har2case -h - name: Run smoketest - postman echo run: | - hrun examples/postman_echo/request_methods + hrun -s examples/postman_echo/request_methods - name: Run smoketest - httpbin run: | - hrun examples/httpbin/ + hrun -s examples/httpbin/ diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2e794dd4..90a7953b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,22 @@ # Release History +## 3.0.5 (2020-05-22) + +**Added** + +- feat: each testcase has an unique id in uuid4 format +- feat: add default header `HRUN-Request-ID` for each testcase #721 +- feat: builtin allure report +- feat: dump log for each testcase + +**Fixed** + +- fix: ensure referenced testcase share the same session + +**Changed** + +- change: remove default added `-s` option for hrun + ## 3.0.4 (2020-05-19) **Added** diff --git a/examples/postman_echo/conftest.py b/examples/postman_echo/conftest.py new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/examples/postman_echo/conftest.py @@ -0,0 +1,2 @@ + + diff --git a/examples/postman_echo/request_methods/conf.py b/examples/postman_echo/request_methods/conf.py deleted file mode 100644 index e69de29b..00000000 diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 1c9ba758..36b1d4d9 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.0.4" +__version__ = "3.0.5" __description__ = "One-stop solution for HTTP(S) testing." from httprunner.runner import HttpRunner diff --git a/httprunner/cli.py b/httprunner/cli.py index f5f5655f..a8140484 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -5,9 +5,9 @@ import sys import pytest from loguru import logger -from httprunner import __description__, __version__, exceptions +from httprunner import __description__, __version__ from httprunner.ext.har2case import init_har2case_parser, main_har2case -from httprunner.ext.make import init_make_parser, main_make, convert_testcase_path +from httprunner.ext.make import init_make_parser, main_make from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold @@ -40,9 +40,6 @@ def main_run(extra_args): sys.exit(1) extra_args_new.extend(testcase_path_list) - if "-s" not in extra_args_new: - extra_args_new.insert(0, "-s") - pytest.main(extra_args_new) diff --git a/httprunner/cli_test.py b/httprunner/cli_test.py index db7f9651..a344866c 100644 --- a/httprunner/cli_test.py +++ b/httprunner/cli_test.py @@ -2,6 +2,8 @@ import io import sys import unittest +import pytest + from httprunner.cli import main @@ -36,3 +38,11 @@ class TestCli(unittest.TestCase): from httprunner import __description__ self.assertIn(__description__, self.captured_output.getvalue().strip()) + + def test_debug_pytest(self): + pytest.main( + [ + "-s", + "examples/postman_echo/request_methods/request_with_variables_test.py", + ] + ) diff --git a/httprunner/client.py b/httprunner/client.py index b7e645a7..f5474ce3 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -41,6 +41,7 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: # record actual request info request_headers = dict(resp_obj.request.headers) + request_cookies = dict(resp_obj.request._cookies) request_body = resp_obj.request.body try: request_body = json.loads(request_body) @@ -57,6 +58,7 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData: method=resp_obj.request.method, url=resp_obj.request.url, headers=request_headers, + cookies=request_cookies, body=request_body, ) @@ -192,7 +194,7 @@ class HttpSession(requests.Session): logger.info( f"status_code: {response.status_code}, " f"response_time(ms): {response_time_ms} ms, " - f"response_length: {content_size} bytes\n" + f"response_length: {content_size} bytes" ) return response diff --git a/httprunner/loader.py b/httprunner/loader.py index 937fabb0..5e6234ed 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -410,6 +410,11 @@ def load_project_meta(test_path: Text) -> ProjectMeta: environments and debugtalk.py functions. """ + project_meta = ProjectMeta() + + if not test_path: + return project_meta + if test_path in project_meta_cached_mapping: return project_meta_cached_mapping[test_path] @@ -417,8 +422,6 @@ def load_project_meta(test_path: Text) -> ProjectMeta: test_path ) - project_meta = ProjectMeta() - # load .env file # NOTICE: # environment variable maybe loaded in debugtalk.py diff --git a/httprunner/runner.py b/httprunner/runner.py index df2824f5..f3f2fb3c 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -1,5 +1,7 @@ import os import time +import uuid +import allure from datetime import datetime from typing import List, Dict, Text @@ -31,11 +33,15 @@ class HttpRunner(object): success: bool = True # indicate testcase execution result __project_meta: ProjectMeta = None + __case_id: Text = "" __step_datas: List[StepData] = None __session: HttpSession = None __session_variables: VariablesMapping = {} - __start_at = 0 - __duration = 0 + # time + __start_at: float = 0 + __duration: float = 0 + # log + __log_path: Text = "" def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": self.__project_meta = project_meta @@ -45,6 +51,10 @@ class HttpRunner(object): self.__session = session return self + def with_case_id(self, case_id: Text) -> "HttpRunner": + self.__case_id = case_id + return self + def with_variables(self, variables: VariablesMapping) -> "HttpRunner": self.__session_variables = variables return self @@ -60,6 +70,10 @@ class HttpRunner(object): parsed_request_dict = parse_data( request_dict, step.variables, self.__project_meta.functions ) + parsed_request_dict["headers"].setdefault( + "HRUN-Request-ID", + f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}", + ) # prepare arguments method = parsed_request_dict.pop("method") @@ -68,7 +82,6 @@ class HttpRunner(object): parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) # request - self.__session = self.__session or HttpSession() resp = self.__session.request(method, url, **parsed_request_dict) resp_obj = ResponseObject(resp) @@ -132,6 +145,7 @@ class HttpRunner(object): case_result = ( HttpRunner() .with_session(self.__session) + .with_case_id(self.__case_id) .with_variables(step_variables) .run_path(ref_testcase_path) ) @@ -159,32 +173,32 @@ class HttpRunner(object): logger.info(f"run step end: {step.name} <<<<<<\n") return step_data.export + def __parse_config(self, config: TConfig): + config.variables.update(self.__session_variables) + config.variables = parse_variables_mapping( + config.variables, self.__project_meta.functions + ) + config.name = parse_data( + config.name, config.variables, self.__project_meta.functions + ) + config.base_url = parse_data( + config.base_url, config.variables, self.__project_meta.functions + ) + def run(self, testcase: TestCase): - """main entrance""" + """run testcase""" self.config = testcase.config self.teststeps = testcase.teststeps - self.config.variables.update(self.__session_variables) - if self.config.path: - self.__project_meta = load_project_meta(self.config.path) - elif not self.__project_meta: - self.__project_meta = ProjectMeta() - - def parse_config(config: TConfig): - config.variables = parse_variables_mapping( - config.variables, self.__project_meta.functions - ) - config.name = parse_data( - config.name, config.variables, self.__project_meta.functions - ) - config.base_url = parse_data( - config.base_url, config.variables, self.__project_meta.functions - ) - - parse_config(self.config) + # prepare + self.__project_meta = self.__project_meta or load_project_meta(self.config.path) + self.__parse_config(self.config) self.__start_at = time.time() self.__step_datas: List[StepData] = [] + self.__session = self.__session or HttpSession() self.__session_variables = {} + + # run teststeps for step in self.teststeps: # update with config variables step.variables.update(self.config.variables) @@ -195,7 +209,8 @@ class HttpRunner(object): step.variables, self.__project_meta.functions ) # run step - extract_mapping = self.__run_step(step) + with allure.step(f"step: {step.name}"): + extract_mapping = self.__run_step(step) # save extracted variables to session variables self.__session_variables.update(extract_mapping) @@ -231,6 +246,7 @@ class HttpRunner(object): return TestCaseSummary( name=self.config.name, success=self.success, + case_id=self.__case_id, time=TestCaseTime( start_at=self.__start_at, start_at_iso_format=start_at_iso_format, @@ -239,9 +255,36 @@ class HttpRunner(object): in_out=TestCaseInOut( vars=self.config.variables, export=self.get_export_variables() ), + log=self.__log_path, step_datas=self.__step_datas, ) def test_start(self): - """discovered by pytest""" - return self.run(TestCase(config=self.config, teststeps=self.teststeps)) + """main entrance, discovered by pytest""" + self.__case_id = self.__case_id or str(uuid.uuid4()) + self.__log_path = self.__log_path or os.path.join( + "logs", f"{self.__case_id}.run.log" + ) + log_handler = logger.add(self.__log_path, level="DEBUG") + + # parse config name + self.__project_meta = self.__project_meta or load_project_meta(self.config.path) + variables = self.config.variables + variables.update(self.__session_variables) + self.config.name = parse_data( + self.config.name, variables, self.__project_meta.functions + ) + + # update allure report meta + allure.dynamic.title(self.config.name) + allure.dynamic.description(f"TestCase ID: {self.__case_id}") + + logger.info( + f"Start to run testcase: {self.config.name}, TestCase ID: {self.__case_id}" + ) + + try: + return self.run(TestCase(config=self.config, teststeps=self.teststeps)) + finally: + logger.remove(log_handler) + logger.info(f"generate testcase log: {self.__log_path}") diff --git a/httprunner/schema.py b/httprunner/schema.py index feae1fb5..c6c0fe97 100644 --- a/httprunner/schema.py +++ b/httprunner/schema.py @@ -13,6 +13,7 @@ BaseUrl = Union[HttpUrl, Text] VariablesMapping = Dict[Text, Any] FunctionsMapping = Dict[Text, Callable] Headers = Dict[Text, Text] +Cookies = Dict[Text, Text] Verify = bool Hook = List[Text] Export = List[Text] @@ -53,7 +54,7 @@ class Request(BaseModel): headers: Headers = {} req_json: Dict = Field({}, alias="json") data: Union[Text, Dict[Text, Any]] = "" - cookies: Dict[Text, Text] = {} + cookies: Cookies = {} timeout: int = 120 allow_redirects: bool = True verify: Verify = False @@ -108,15 +109,15 @@ class RequestData(BaseModel): method: MethodEnum = MethodEnum.GET url: Url headers: Headers = {} - # TODO: add cookies + cookies: Cookies = {} body: Union[Text, bytes, Dict, None] = {} class ResponseData(BaseModel): status_code: int - cookies: Dict - encoding: Union[Text, None] = None headers: Dict + cookies: Cookies + encoding: Union[Text, None] = None content_type: Text body: Union[Text, bytes, Dict] @@ -147,8 +148,9 @@ class StepData(BaseModel): class TestCaseSummary(BaseModel): - name: Text = "" - success: bool = False + name: Text + success: bool + case_id: Text time: TestCaseTime in_out: TestCaseInOut = {} log: Text = "" diff --git a/poetry.lock b/poetry.lock index 8cc0bcd1..e373caa9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,32 @@ version = "0.2.2" python = "<3.7" version = "2.4" +[[package]] +category = "main" +description = "Allure pytest integration" +name = "allure-pytest" +optional = false +python-versions = "*" +version = "2.8.15" + +[package.dependencies] +allure-python-commons = "2.8.15" +pytest = ">=4.5.0" +six = ">=1.9.0" + +[[package]] +category = "main" +description = "Common module for integrate allure with python-based frameworks" +name = "allure-python-commons" +optional = false +python-versions = "*" +version = "2.8.15" + +[package.dependencies] +attrs = ">=16.0.0" +pluggy = ">=0.4.0" +six = ">=1.9.0" + [[package]] category = "main" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -516,7 +542,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "be53fb0cd423bac9dda129a958a58026009a99a455081333d7af51c22a4df8cf" +content-hash = "67027f8f78c61b981f3c01613ded1da2a0256a28fb92f95dd2d642b3fd1b43a5" python-versions = "^3.6" [metadata.files] @@ -524,6 +550,14 @@ aiocontextvars = [ {file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"}, {file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"}, ] +allure-pytest = [ + {file = "allure-pytest-2.8.15.tar.gz", hash = "sha256:27f9c75194e95ba069ee2d6d2a2615ed6c7e96617ff9a492ab3a74f3f4e64be2"}, + {file = "allure_pytest-2.8.15-py3-none-any.whl", hash = "sha256:62512bbce3d39b27a8e7ffbfb24e08e99c43df29b4f345168dfc9692bfddef71"}, +] +allure-python-commons = [ + {file = "allure-python-commons-2.8.15.tar.gz", hash = "sha256:c4768e5e1350fe2eb6e1c9dac6158dcb82e23de80c83c4fc6d71765c207c1408"}, + {file = "allure_python_commons-2.8.15-py3-none-any.whl", hash = "sha256:88ad53109b6fa57e6b721f4eab59116db6037e219bf54e1f196a222ba5e2dcfe"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, diff --git a/pyproject.toml b/pyproject.toml index 6614464f..9a499654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "httprunner" -version = "3.0.4" +version = "3.0.5" description = "One-stop solution for HTTP(S) testing." license = "Apache-2.0" readme = "README.md" @@ -39,6 +39,7 @@ loguru = "^0.4.1" jmespath = "^0.9.5" black = "^19.10b0" pytest = "^5.4.2" +allure-pytest = "^2.8.15" [tool.poetry.dev-dependencies] coverage = "^4.5.4"