diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1caebbe1..d17b5df1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,8 @@ - change: remove har2case, move all features to go version, replace with `hrp run` - change: remove locust, you should run load tests with go version, replace with `hrp boom` - change: remove fastapi and uvicorn dependencies +- change: add pytest.ini to make log colorful +- feat: support retry when test step failed - fix: ignore exceptions when reporting GA events - fix: remove misuse of NoReturn in Python typing diff --git a/examples/postman_echo/debugtalk.py b/examples/postman_echo/debugtalk.py index c0b2aebb..f17806a6 100644 --- a/examples/postman_echo/debugtalk.py +++ b/examples/postman_echo/debugtalk.py @@ -23,3 +23,20 @@ def get_app_version(): def calculate_two_nums(a, b=1): return [a + b, b - a] + + +def fake_rand_count(): + """ + return 1 at first call + return 2 at second call + """ + l = [] + + def func(): + l.append(1) + return len(l) + + return func + + +fake_randnum = fake_rand_count() diff --git a/examples/postman_echo/request_methods/request_with_retry_test.py b/examples/postman_echo/request_methods/request_with_retry_test.py new file mode 100644 index 00000000..e112501a --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_retry_test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" + + @Date : 2022/4/7 + @File : request_with_retry.py + @Author : duanchao.bill + @Desc : + +""" + +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase + + +class TestCaseRetry(HttpRunner): + config = ( + Config("request methods testcase in hardcode") + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("run with retry") + .with_retry(retry_times=1, retry_interval=1) + .get("/get") + .with_params(**{"foo1": "${fake_randnum()}"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .validate() + .assert_equal("body.args.foo1", "2") + ) + ] diff --git a/examples/pytest.ini b/examples/pytest.ini new file mode 100644 index 00000000..0aad31f7 --- /dev/null +++ b/examples/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +addopts = -s +# https://docs.pytest.org/en/latest/how-to/output.html +junit_logging = all +junit_duration_report = total +log_cli = False diff --git a/httprunner/models.py b/httprunner/models.py index 578fe30b..a1f7ecd9 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -82,6 +82,9 @@ class TStep(BaseModel): export: Export = [] validators: Validators = Field([], alias="validate") validate_script: List[Text] = [] + retry_times: int = 0 + retry_interval: int = 0 # sec + class TestCase(BaseModel): diff --git a/httprunner/runner.py b/httprunner/runner.py index 4c90bb9b..53d4786c 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -12,10 +12,9 @@ except ModuleNotFoundError: USE_ALLURE = False from loguru import logger - from httprunner.client import HttpSession from httprunner.config import Config -from httprunner.exceptions import ParamsError +from httprunner.exceptions import ParamsError, ValidationFailure from httprunner.loader import load_project_meta from httprunner.models import (ProjectMeta, StepResult, TConfig, TestCaseInOut, TestCaseSummary, TestCaseTime, VariablesMapping) @@ -25,7 +24,7 @@ from httprunner.utils import merge_variables class SessionRunner(object): config: Config - teststeps: List[object] # list of Step + teststeps: List[object] # list of Step parser: Parser = None session: HttpSession = None @@ -162,11 +161,23 @@ class SessionRunner(object): logger.info(f"run step begin: {step.name()} >>>>>>") # run step - if USE_ALLURE: - with allure.step(f"step: {step.name()}"): - step_result: StepResult = step.run(self) - else: - step_result: StepResult = step.run(self) + for i in range(step.retry_times + 1): + try: + if USE_ALLURE: + with allure.step(f"step: {step.name()}"): + step_result: StepResult = step.run(self) + else: + step_result: StepResult = step.run(self) + break + except ValidationFailure: + if i == step.retry_times: + raise + else: + logger.warning( + f"run step {step.name()} validation failed,wait {step.retry_interval} sec and try again") + time.sleep(step.retry_interval) + logger.info( + f"run step retry ({i+1}/{step.retry_times} time): {step.name()} >>>>>>") # save extracted variables to session variables self.__session_variables.update(step_result.export_vars) diff --git a/httprunner/step.py b/httprunner/step.py index f9721122..c349de3e 100644 --- a/httprunner/step.py +++ b/httprunner/step.py @@ -27,6 +27,14 @@ class Step(object): def testcase(self) -> TestCase: return self.__step.struct().testcase + @property + def retry_times(self) -> int: + return self.__step.struct().retry_times + + @property + def retry_interval(self) -> int: + return self.__step.struct().retry_interval + def struct(self) -> TStep: return self.__step.struct() diff --git a/httprunner/step_request.py b/httprunner/step_request.py index 419d7bab..a681e448 100644 --- a/httprunner/step_request.py +++ b/httprunner/step_request.py @@ -426,6 +426,11 @@ class RunRequest(object): self.__step.variables.update(variables) return self + def with_retry(self, retry_times, retry_interval) -> "RunRequest": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest": if assign_var_name: self.__step.setup_hooks.append({assign_var_name: hook}) diff --git a/httprunner/step_testcase.py b/httprunner/step_testcase.py index 8ae59eb4..f661f6a3 100644 --- a/httprunner/step_testcase.py +++ b/httprunner/step_testcase.py @@ -80,6 +80,11 @@ class RunTestCase(object): self.__step.variables.update(variables) return self + def with_retry(self, retry_times, retry_interval) -> "RunRequest": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase": if assign_var_name: self.__step.setup_hooks.append({assign_var_name: hook})