From 03b8aa3518863056d404b3273fb6936875b00070 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 31 Mar 2022 12:11:18 +0800 Subject: [PATCH 01/12] change: move step logs to session runner --- docs/dev.md => hrp/README.md | 0 hrp/response.go | 14 +++++++------- hrp/session.go | 28 ++++++++++++++++++++++++---- hrp/step_api.go | 4 ---- hrp/step_request.go | 12 ------------ hrp/step_testcase.go | 9 --------- hrp/step_thinktime.go | 3 +-- 7 files changed, 32 insertions(+), 38 deletions(-) rename docs/dev.md => hrp/README.md (100%) diff --git a/docs/dev.md b/hrp/README.md similarity index 100% rename from docs/dev.md rename to hrp/README.md diff --git a/hrp/response.go b/hrp/response.go index 37164eb0..7717b5da 100644 --- a/hrp/response.go +++ b/hrp/response.go @@ -173,13 +173,13 @@ func (v *responseObject) Validate(iValidators []interface{}, variablesMapping ma Msgf("validate %s", checkItem) if !result { v.t.Fail() - return errors.New(fmt.Sprintf( - "do assertion failed, checkExpr: %v, assertMethod: %v, checkValue: %v, expectValue: %v", - validator.Check, - assertMethod, - checkValue, - expectValue, - )) + log.Error(). + Str("checkExpr", validator.Check). + Str("assertMethod", assertMethod). + Interface("checkValue", checkValue). + Interface("expectValue", expectValue). + Msg("assert failed") + return errors.New("step validation failed") } } return nil diff --git a/hrp/session.go b/hrp/session.go index b65f92d4..78674bf2 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -69,24 +69,44 @@ func (r *SessionRunner) Start() error { r.startTime = time.Now() // run step in sequential order for _, step := range r.testCase.TestSteps { - _, err := step.Run(r) + log.Info().Str("step", step.Name()). + Str("type", string(step.Type())).Msg("run step start") + + stepResult, err := step.Run(r) if err != nil && r.hrpRunner.failfast { + log.Error(). + Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", false). + Msg("run step end") return errors.Wrap(err, "abort running due to failfast setting") } + + // update extracted variables + r.updateSession(stepResult.ExportVars) + // update testcase summary + r.updateSummary(stepResult) + + log.Info(). + Str("step", stepResult.Name). + Str("type", string(stepResult.StepType)). + Bool("success", stepResult.Success). + Interface("exportVars", stepResult.ExportVars). + Msg("run step end") } log.Info().Str("testcase", config.Name).Msg("run testcase end") return nil } -func (r *SessionRunner) UpdateSession(vars map[string]interface{}) { +func (r *SessionRunner) updateSession(vars map[string]interface{}) { for k, v := range vars { r.sessionVariables[k] = v } } -// UpdateSummary appends step result to summary -func (r *SessionRunner) UpdateSummary(stepResult *StepResult) { +// updateSummary appends step result to summary +func (r *SessionRunner) updateSummary(stepResult *StepResult) { r.summary.Records = append(r.summary.Records, stepResult) r.summary.Stat.Total += 1 if stepResult.Success { diff --git a/hrp/step_api.go b/hrp/step_api.go index 0bd8cead..d74406f3 100644 --- a/hrp/step_api.go +++ b/hrp/step_api.go @@ -3,8 +3,6 @@ package hrp import ( "fmt" - "github.com/rs/zerolog/log" - "github.com/httprunner/httprunner/hrp/internal/builtin" ) @@ -93,8 +91,6 @@ func (s *StepAPIWithOptionalArgs) Struct() *TStep { } func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) { - log.Info().Str("api", s.step.Name).Msg("run referenced api") - // extend request with referenced API api, _ := s.step.API.(*API) extendWithAPI(s.step, api) diff --git a/hrp/step_request.go b/hrp/step_request.go index 372bd50e..6639740d 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -243,8 +243,6 @@ func (r *requestBuilder) prepareBody(stepVariables map[string]interface{}) error } func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { - log.Info().Str("step", step.Name).Msg("run step start") - stepResult = &StepResult{ Name: step.Name, StepType: stepTypeRequest, @@ -255,18 +253,8 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err defer func() { // update testcase summary if err != nil { - log.Error().Err(err).Msg("run request step failed") stepResult.Attachment = err.Error() - } else { - // update extracted variables - r.UpdateSession(stepResult.ExportVars) - log.Info(). - Str("step", step.Name). - Bool("success", stepResult.Success). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") } - r.UpdateSummary(stepResult) }() sessionData := newSessionData() diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index bd69eb56..9d96256f 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -50,7 +50,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error } s.step.Variables = stepVariables - log.Info().Str("testcase", s.step.Name).Msg("run referenced testcase") stepResult := &StepResult{ Name: s.step.Name, StepType: stepTypeTestCase, @@ -73,8 +72,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error err = sessionRunner.Start() stepResult.Elapsed = time.Since(start).Milliseconds() if err != nil { - log.Error().Err(err).Msg("run referenced testcase step failed") - log.Info().Str("step", s.step.Name).Bool("success", false).Msg("run step end") stepResult.Attachment = err.Error() r.summary.Success = false return stepResult, err @@ -96,12 +93,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error r.summary.Stat.Successes += summary.Stat.Successes r.summary.Stat.Failures += summary.Stat.Failures - log.Info(). - Str("step", s.step.Name). - Bool("success", true). - Interface("exportVars", stepResult.ExportVars). - Msg("run step end") - return stepResult, nil } diff --git a/hrp/step_thinktime.go b/hrp/step_thinktime.go index 3e54f7ee..ad158061 100644 --- a/hrp/step_thinktime.go +++ b/hrp/step_thinktime.go @@ -30,8 +30,7 @@ func (s *StepThinkTime) Struct() *TStep { func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { thinkTime := s.step.ThinkTime - log.Info().Str("name", s.step.Name). - Float64("time", thinkTime.Time).Msg("think time") + log.Info().Float64("time", thinkTime.Time).Msg("think time") stepResult := &StepResult{ Name: s.step.Name, From d36c1cb98767894129fe93c4014c586ec4f61e6e Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 1 Apr 2022 22:47:47 +0800 Subject: [PATCH 02/12] refactor: make step extensible to support implementing new protocols and test types for python version --- docs/CHANGELOG.md | 2 +- hrp/step_request.go | 12 +- hrp/step_testcase.go | 4 +- httprunner/__init__.py | 6 +- httprunner/config.py | 60 +++++ httprunner/models.py | 24 +- httprunner/parser.py | 26 +- httprunner/response.py | 24 +- httprunner/runner.py | 459 ++++++++--------------------------- httprunner/step.py | 40 ++++ httprunner/step_request.py | 462 ++++++++++++++++++++++++++++++++++++ httprunner/step_testcase.py | 110 +++++++++ httprunner/testcase.py | 415 -------------------------------- 13 files changed, 842 insertions(+), 802 deletions(-) create mode 100644 httprunner/config.py create mode 100644 httprunner/step.py create mode 100644 httprunner/step_request.py create mode 100644 httprunner/step_testcase.py delete mode 100644 httprunner/testcase.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3f986f07..d938d777 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ ## v4.0.0-alpha - refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine +- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types **go version** @@ -11,7 +12,6 @@ - change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis - change: lock funplugin version when creating scaffold project - fix: call referenced api/testcase with relative path -- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types **python version** diff --git a/hrp/step_request.go b/hrp/step_request.go index 6639740d..78b205c4 100644 --- a/hrp/step_request.go +++ b/hrp/step_request.go @@ -257,6 +257,12 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err } }() + // override step variables + stepVariables, err := r.MergeStepVariables(step.Variables) + if err != nil { + return + } + sessionData := newSessionData() parser := r.GetParser() config := r.GetConfig() @@ -264,12 +270,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err rb := newRequestBuilder(parser, config, step.Request) rb.req.Method = string(step.Request.Method) - // override step variables - stepVariables, err := r.MergeStepVariables(step.Variables) - if err != nil { - return - } - err = rb.prepareUrlParams(stepVariables) if err != nil { return diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index 9d96256f..ea412058 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -77,9 +77,9 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error return stepResult, err } summary := sessionRunner.GetSummary() - stepResult.Data = summary + stepResult.Data = summary.Records // export testcase export variables - stepResult.ExportVars = sessionRunner.summary.InOut.ExportVars + stepResult.ExportVars = summary.InOut.ExportVars stepResult.Success = true // update extracted variables diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 6f10a6f5..5c6dcfe5 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,10 +1,12 @@ __version__ = "4.0.0-alpha" __description__ = "One-stop solution for HTTP(S) testing." -# import firstly for monkey patch if needed +from httprunner.config import Config from httprunner.parser import parse_parameters as Parameters from httprunner.runner import HttpRunner -from httprunner.testcase import Config, Step, RunRequest, RunTestCase +from httprunner.step import Step +from httprunner.step_request import RunRequest +from httprunner.step_testcase import RunTestCase __all__ = [ "__version__", diff --git a/httprunner/config.py b/httprunner/config.py new file mode 100644 index 00000000..2a449ddb --- /dev/null +++ b/httprunner/config.py @@ -0,0 +1,60 @@ +import inspect +from typing import Text + +from httprunner.models import TConfig + + +class Config(object): + def __init__(self, name: Text): + self.__name = name + self.__variables = {} + self.__base_url = "" + self.__verify = False + self.__export = [] + self.__weight = 1 + + caller_frame = inspect.stack()[1] + self.__path = caller_frame.filename + + @property + def name(self) -> Text: + return self.__name + + @property + def path(self) -> Text: + return self.__path + + @property + def weight(self) -> int: + return self.__weight + + def variables(self, **variables) -> "Config": + self.__variables.update(variables) + return self + + def base_url(self, base_url: Text) -> "Config": + self.__base_url = base_url + return self + + def verify(self, verify: bool) -> "Config": + self.__verify = verify + return self + + def export(self, *export_var_name: Text) -> "Config": + self.__export.extend(export_var_name) + return self + + def locust_weight(self, weight: int) -> "Config": + self.__weight = weight + return self + + def struct(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, + weight=self.__weight, + ) diff --git a/httprunner/models.py b/httprunner/models.py index e149e407..95d2fb30 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -156,15 +156,35 @@ class SessionData(BaseModel): class StepData(BaseModel): """teststep data, each step maybe corresponding to one request or one testcase""" + name: Text = "" # teststep name + step_type: Text = "" # teststep type, request or testcase success: bool = False - name: Text = "" # teststep name data: Union[SessionData, List['StepData']] = None + elapsed: float = 0.0 # teststep elapsed time + content_size: float = 0 # response content size export_vars: VariablesMapping = {} + attachment: Text = "" # teststep attachment + - StepData.update_forward_refs() +class IStep(object): + + def name(self) -> str: + raise NotImplementedError + + def type(self) -> str: + raise NotImplementedError + + def struct(self) -> TStep: + raise NotImplementedError + + def run(self, runner) -> StepData: + # runner: HttpRunner + raise NotImplementedError + + class TestCaseSummary(BaseModel): name: Text success: bool diff --git a/httprunner/parser.py b/httprunner/parser.py index 457d8806..9f32d721 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -1,14 +1,14 @@ import ast import builtins -import re import os -from typing import Any, Set, Text, Callable, List, Dict, Union +import re +from typing import Any, Callable, Dict, List, Set, Text from loguru import logger from sentry_sdk import capture_exception -from httprunner import loader, utils, exceptions -from httprunner.models import VariablesMapping, FunctionsMapping +from httprunner import exceptions, loader, utils +from httprunner.models import FunctionsMapping, VariablesMapping absolute_http_url_regexp = re.compile(r"^https?://", re.I) @@ -572,3 +572,21 @@ def parse_parameters(parameters: Dict,) -> List[Dict]: parsed_parameters_list.append(parameter_content_list) return utils.gen_cartesian_product(*parsed_parameters_list) + + +class Parser(object): + + def __init__(self, functions_mapping: FunctionsMapping = None) -> None: + self.functions_mapping = functions_mapping + + def parse_string(self, raw_string: Text, variables_mapping: VariablesMapping) -> Any: + return parse_string(raw_string, variables_mapping, self.functions_mapping) + + def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping: + return parse_variables_mapping(variables_mapping, self.functions_mapping) + + def parse_data(self, raw_data: Any, variables_mapping: VariablesMapping = None) -> Any: + return parse_data(raw_data, variables_mapping, self.functions_mapping) + + def get_mapping_function(self, func_name: Text) -> Callable: + return get_mapping_function(func_name, self.functions_mapping) diff --git a/httprunner/response.py b/httprunner/response.py index 7fd43bc5..fb711dc9 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -7,8 +7,8 @@ from loguru import logger from httprunner import exceptions from httprunner.exceptions import ValidationFailure, ParamsError -from httprunner.models import VariablesMapping, Validators, FunctionsMapping -from httprunner.parser import parse_data, parse_string_value, get_mapping_function +from httprunner.models import VariablesMapping, Validators +from httprunner.parser import parse_string_value, Parser def get_uniform_comparator(comparator: Text): @@ -115,7 +115,7 @@ def uniform_validator(validator): class ResponseObject(object): - def __init__(self, resp_obj: requests.Response): + def __init__(self, resp_obj: requests.Response, parser: Parser): """ initialize with a requests.Response object Args: @@ -123,6 +123,7 @@ class ResponseObject(object): """ self.resp_obj = resp_obj + self.parser = parser self.validation_results: Dict = {} def __getattr__(self, key): @@ -170,7 +171,6 @@ class ResponseObject(object): def extract(self, extractors: Dict[Text, Text], variables_mapping: VariablesMapping = None, - functions_mapping: FunctionsMapping = None, ) -> Dict[Text, Any]: if not extractors: return {} @@ -179,8 +179,8 @@ class ResponseObject(object): for key, field in extractors.items(): if '$' in field: # field contains variable or function - field = parse_data( - field, variables_mapping, functions_mapping + field = self.parser.parse_data( + field, variables_mapping ) field_value = self._search_jmespath(field) extract_mapping[key] = field_value @@ -192,11 +192,9 @@ class ResponseObject(object): self, validators: Validators, variables_mapping: VariablesMapping = None, - functions_mapping: FunctionsMapping = None, ): variables_mapping = variables_mapping or {} - functions_mapping = functions_mapping or {} self.validation_results = {} if not validators: @@ -216,8 +214,8 @@ class ResponseObject(object): check_item = u_validator["check"] if "$" in check_item: # check_item is variable or function - check_item = parse_data( - check_item, variables_mapping, functions_mapping + check_item = self.parser.parse_data( + check_item, variables_mapping ) check_item = parse_string_value(check_item) @@ -229,17 +227,17 @@ class ResponseObject(object): # comparator assert_method = u_validator["assert"] - assert_func = get_mapping_function(assert_method, functions_mapping) + assert_func = self.parser.get_mapping_function(assert_method) # expect item expect_item = u_validator["expect"] # parse expected value with config/teststep/extracted variables - expect_value = parse_data(expect_item, variables_mapping, functions_mapping) + expect_value = self.parser.parse_data(expect_item, variables_mapping) # message message = u_validator["message"] # parse message with config/teststep/extracted variables - message = parse_data(message, variables_mapping, functions_mapping) + message = self.parser.parse_data(message, variables_mapping) validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})" diff --git a/httprunner/runner.py b/httprunner/runner.py index 3c5600ad..af9b6ee7 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -2,7 +2,7 @@ import os import time import uuid from datetime import datetime -from typing import List, Dict, Text +from typing import Dict, List, Text try: import allure @@ -13,41 +13,29 @@ except ModuleNotFoundError: from loguru import logger -from httprunner import utils, exceptions from httprunner.client import HttpSession -from httprunner.exceptions import ValidationFailure, ParamsError -from httprunner.ext.uploader import prepare_upload_step -from httprunner.loader import load_project_meta, load_testcase_file -from httprunner.parser import build_url, parse_data, parse_variables_mapping -from httprunner.response import ResponseObject -from httprunner.testcase import Config, Step +from httprunner.config import Config +from httprunner.exceptions import ParamsError +from httprunner.loader import load_project_meta +from httprunner.models import (ProjectMeta, StepData, TConfig, TestCaseInOut, + TestCaseSummary, TestCaseTime, VariablesMapping) +from httprunner.parser import Parser from httprunner.utils import merge_variables -from httprunner.models import ( - TConfig, - TStep, - VariablesMapping, - StepData, - TestCaseSummary, - TestCaseTime, - TestCaseInOut, - ProjectMeta, - TestCase, - Hooks, -) class HttpRunner(object): config: Config - teststeps: List[Step] + teststeps: List[object] # list of Step + + parser: Parser = None + session: HttpSession = None + case_id: Text = "" + root_dir: Text = "" - success: bool = False # indicate testcase execution result __config: TConfig - __teststeps: List[TStep] __project_meta: ProjectMeta = None - __case_id: Text = "" __export: List[Text] = [] __step_datas: List[StepData] = [] - __session: HttpSession = None __session_variables: VariablesMapping = {} # time __start_at: float = 0 @@ -55,29 +43,34 @@ class HttpRunner(object): # log __log_path: Text = "" - def __init_tests__(self): - self.__config = self.config.perform() - self.__teststeps = [] - for step in self.teststeps: - self.__teststeps.append(step.perform()) + def __init(self): + self.__config = self.config.struct() + self.__session_variables = {} + self.__start_at = 0 + self.__duration = 0 - @property - def raw_testcase(self) -> TestCase: - if not hasattr(self, "__config"): - self.__init_tests__() + self.__project_meta = self.__project_meta or load_project_meta( + self.__config.path + ) + self.case_id = self.case_id or str(uuid.uuid4()) + self.root_dir = self.root_dir or self.__project_meta.RootDir + self.__log_path = os.path.join( + self.root_dir, "logs", f"{self.case_id}.run.log" + ) - return TestCase(config=self.__config, teststeps=self.__teststeps) - - def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": - self.__project_meta = project_meta - return self + self.__step_datas.clear() + self.session = self.session or HttpSession() + self.parser = self.parser or Parser(self.__project_meta.functions) def with_session(self, session: HttpSession) -> "HttpRunner": - self.__session = session + self.session = session return self + def get_config(self) -> TConfig: + return self.__config + def with_case_id(self, case_id: Text) -> "HttpRunner": - self.__case_id = case_id + self.case_id = case_id return self def with_variables(self, variables: VariablesMapping) -> "HttpRunner": @@ -88,302 +81,24 @@ class HttpRunner(object): self.__export = export return self - def __call_hooks(self, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text): - """ call hook actions. - - Args: - hooks (list): each hook in hooks list maybe in two format. - - format1 (str): only call hook functions. - ${func()} - format2 (dict): assignment, the value returned by hook function will be assigned to variable. - {"var": "${func()}"} - - step_variables: current step variables to call hook, include two special variables - - request: parsed request dict - response: ResponseObject for current response - - hook_msg: setup/teardown request/testcase - - """ - logger.info(f"call hook actions: {hook_msg}") - - if not isinstance(hooks, List): - logger.error(f"Invalid hooks format: {hooks}") - return - - for hook in hooks: - if isinstance(hook, Text): - # format 1: ["${func()}"] - logger.debug(f"call hook function: {hook}") - parse_data(hook, step_variables, self.__project_meta.functions) - elif isinstance(hook, Dict) and len(hook) == 1: - # format 2: {"var": "${func()}"} - var_name, hook_content = list(hook.items())[0] - hook_content_eval = parse_data( - hook_content, step_variables, self.__project_meta.functions - ) - logger.debug( - f"call hook function: {hook_content}, got value: {hook_content_eval}" - ) - logger.debug(f"assign variable: {var_name} = {hook_content_eval}") - step_variables[var_name] = hook_content_eval - else: - logger.error(f"Invalid hook format: {hook}") - - def __run_step_request(self, step: TStep) -> StepData: - """run teststep: request""" - step_data = StepData(name=step.name) - - # parse - functions = self.__project_meta.functions - prepare_upload_step(step, functions) - request_dict = step.request.dict() - request_dict.pop("upload", None) - parsed_request_dict = parse_data( - request_dict, step.variables, functions - ) - parsed_request_dict["headers"].setdefault( - "HRUN-Request-ID", - f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}", - ) - step.variables["request"] = parsed_request_dict - - # setup hooks - if step.setup_hooks: - self.__call_hooks(step.setup_hooks, step.variables, "setup request") - - # prepare arguments - 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 - resp = self.__session.request(method, url, **parsed_request_dict) - resp_obj = ResponseObject(resp) - step.variables["response"] = resp_obj - - # teardown hooks - if step.teardown_hooks: - self.__call_hooks(step.teardown_hooks, step.variables, "teardown request") - - def log_req_resp_details(): - err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32) - - # log request - err_msg += "====== request details ======\n" - err_msg += f"url: {url}\n" - err_msg += f"method: {method}\n" - headers = parsed_request_dict.pop("headers", {}) - err_msg += f"headers: {headers}\n" - for k, v in parsed_request_dict.items(): - v = utils.omit_long_data(v) - err_msg += f"{k}: {repr(v)}\n" - - err_msg += "\n" - - # log response - err_msg += "====== response details ======\n" - err_msg += f"status_code: {resp.status_code}\n" - err_msg += f"headers: {resp.headers}\n" - err_msg += f"body: {repr(resp.text)}\n" - logger.error(err_msg) - - # extract - extractors = step.extract - extract_mapping = resp_obj.extract(extractors, step.variables, functions) - step_data.export_vars = extract_mapping - - variables_mapping = step.variables - variables_mapping.update(extract_mapping) - - # validate - validators = step.validators - session_success = False - try: - resp_obj.validate( - validators, variables_mapping, functions - ) - session_success = True - except ValidationFailure: - session_success = False - log_req_resp_details() - # log testcase duration before raise ValidationFailure - self.__duration = time.time() - self.__start_at - raise - finally: - self.success = session_success - step_data.success = session_success - - if hasattr(self.__session, "data"): - # httprunner.client.HttpSession, not locust.clients.HttpSession - # save request & response meta data - self.__session.data.success = session_success - self.__session.data.validators = resp_obj.validation_results - - # save step data - step_data.data = self.__session.data - - return step_data - - def __run_step_testcase(self, step: TStep) -> StepData: - """run teststep: referenced testcase""" - step_data = StepData(name=step.name) - step_variables = step.variables - step_export = step.export - - # setup hooks - if step.setup_hooks: - self.__call_hooks(step.setup_hooks, step_variables, "setup testcase") - - if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"): - testcase_cls = step.testcase - case_result = ( - testcase_cls() - .with_session(self.__session) - .with_case_id(self.__case_id) - .with_variables(step_variables) - .with_export(step_export) - .run() - ) - - elif isinstance(step.testcase, Text): - if os.path.isabs(step.testcase): - ref_testcase_path = step.testcase - else: - ref_testcase_path = os.path.join( - self.__project_meta.RootDir, step.testcase - ) - - case_result = ( - HttpRunner() - .with_session(self.__session) - .with_case_id(self.__case_id) - .with_variables(step_variables) - .with_export(step_export) - .run_path(ref_testcase_path) - ) - - else: - raise exceptions.ParamsError( - f"Invalid teststep referenced testcase: {step.dict()}" - ) - - # teardown hooks - if step.teardown_hooks: - self.__call_hooks(step.teardown_hooks, step.variables, "teardown testcase") - - step_data.data = case_result.get_step_datas() # list of step data - step_data.export_vars = case_result.get_export_variables() - step_data.success = case_result.success - self.success = case_result.success - - if step_data.export_vars: - logger.info(f"export variables: {step_data.export_vars}") - - return step_data - - def __run_step(self, step: TStep) -> Dict: - """run teststep, teststep maybe a request or referenced testcase""" - logger.info(f"run step begin: {step.name} >>>>>>") - - if step.request: - step_data = self.__run_step_request(step) - elif step.testcase: - step_data = self.__run_step_testcase(step) - else: - raise ParamsError( - f"teststep is neither a request nor a referenced testcase: {step.dict()}" - ) - - self.__step_datas.append(step_data) - logger.info(f"run step end: {step.name} <<<<<<\n") - return step_data.export_vars - - 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 __parse_config(self, param: Dict = None) -> None: + # parse config variables + self.__config.variables.update(self.__session_variables) + if param: + self.__config.variables.update(param) + self.__config.variables = self.parser.parse_variables( + self.__config.variables ) - def run_testcase(self, testcase: TestCase) -> "HttpRunner": - """run specified testcase - - Examples: - >>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)]) - >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj) - - """ - self.__config = testcase.config - self.__teststeps = testcase.teststeps - - # prepare - self.__project_meta = self.__project_meta or load_project_meta( - self.__config.path + # parse config name + self.__config.name = self.parser.parse_data( + self.__config.name, self.__config.variables ) - self.__parse_config(self.__config) - self.__start_at = time.time() - self.__step_datas: List[StepData] = [] - self.__session = self.__session or HttpSession() - # save extracted variables of teststeps - extracted_variables: VariablesMapping = {} - # run teststeps - for step in self.__teststeps: - # override variables - # step variables > extracted variables from previous steps - step.variables = merge_variables(step.variables, extracted_variables) - # step variables > testcase config variables - step.variables = merge_variables(step.variables, self.__config.variables) - - # parse variables - step.variables = parse_variables_mapping( - step.variables, self.__project_meta.functions - ) - - # run step - if USE_ALLURE: - with allure.step(f"step: {step.name}"): - extract_mapping = self.__run_step(step) - else: - extract_mapping = self.__run_step(step) - - # save extracted variables to session variables - extracted_variables.update(extract_mapping) - - self.__session_variables.update(extracted_variables) - self.__duration = time.time() - self.__start_at - return self - - def run_path(self, path: Text) -> "HttpRunner": - if not os.path.isfile(path): - raise exceptions.ParamsError(f"Invalid testcase path: {path}") - - testcase_obj = load_testcase_file(path) - return self.run_testcase(testcase_obj) - - def run(self) -> "HttpRunner": - """ run current testcase - - Examples: - >>> TestCaseRequestWithFunctions().run() - - """ - self.__init_tests__() - testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps) - return self.run_testcase(testcase_obj) - - def get_step_datas(self) -> List[StepData]: - return self.__step_datas + # parse config base url + self.__config.base_url = self.parser.parse_data( + self.__config.base_url, self.__config.variables + ) def get_export_variables(self) -> Dict: # override testcase export vars with step export @@ -403,10 +118,17 @@ class HttpRunner(object): """get testcase result summary""" start_at_timestamp = self.__start_at start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat() + + summary_success = True + for step_data in self.__step_datas: + if not step_data.success: + summary_success = False + break + return TestCaseSummary( name=self.__config.name, - success=self.success, - case_id=self.__case_id, + success=summary_success, + case_id=self.case_id, time=TestCaseTime( start_at=self.__start_at, start_at_iso_format=start_at_iso_format, @@ -420,40 +142,63 @@ class HttpRunner(object): step_datas=self.__step_datas, ) - def test_start(self, param: Dict = None) -> "HttpRunner": + def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping: + # override variables + # step variables > extracted variables from previous steps + variables = merge_variables(variables, self.__session_variables) + # step variables > testcase config variables + variables = merge_variables(variables, self.__config.variables) + + # parse variables + return self.parser.parse_variables(variables) + + def __run_step(self, step) -> Dict: + """run teststep, step maybe any kind that implements IStep interface + + Args: + step (Step): teststep + + """ + logger.info(f"run step begin: {step.name()} >>>>>>") + + # run step + if USE_ALLURE: + with allure.step(f"step: {step.name()}"): + step_result = step.run(self) + else: + step_result = step.run(self) + + # save extracted variables to session variables + self.__session_variables.update(step_result.export_vars) + # update testcase summary + self.__step_datas.append(step_result) + + logger.info(f"run step end: {step.name()} <<<<<<\n") + + def test_start(self, param: Dict = None): """main entrance, discovered by pytest""" - self.__init_tests__() - self.__project_meta = self.__project_meta or load_project_meta( - self.__config.path - ) - self.__case_id = self.__case_id or str(uuid.uuid4()) - self.__log_path = self.__log_path or os.path.join( - self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log" - ) + self.__init() log_handler = logger.add(self.__log_path, level="DEBUG") - # parse config name - config_variables = self.__config.variables - if param: - config_variables.update(param) - config_variables.update(self.__session_variables) - self.__config.name = parse_data( - self.__config.name, config_variables, self.__project_meta.functions - ) + self.__parse_config(param) if USE_ALLURE: # update allure report meta allure.dynamic.title(self.__config.name) - allure.dynamic.description(f"TestCase ID: {self.__case_id}") + allure.dynamic.description(f"TestCase ID: {self.case_id}") logger.info( - f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}" + f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}" ) + self.__start_at = time.time() try: - return self.run_testcase( - TestCase(config=self.__config, teststeps=self.__teststeps) - ) + # run step in sequential order + for step in self.teststeps: + self.__run_step(step) finally: logger.remove(log_handler) logger.info(f"generate testcase log: {self.__log_path}") + + self.__duration = time.time() - self.__start_at + return self diff --git a/httprunner/step.py b/httprunner/step.py new file mode 100644 index 00000000..f6bc7c90 --- /dev/null +++ b/httprunner/step.py @@ -0,0 +1,40 @@ +from typing import Union + +from httprunner.models import StepData, TRequest, TStep, TestCase +from httprunner.runner import HttpRunner +from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation +from httprunner.step_testcase import StepRefCase + + +class Step(object): + + def __init__( + self, + step: Union[ + StepRequestValidation, + StepRequestExtraction, + RequestWithOptionalArgs, + StepRefCase, + ], + ): + self.__step = step + + @property + def request(self) -> TRequest: + return self.__step.struct().request + + @property + def testcase(self) -> TestCase: + return self.__step.struct().testcase + + def struct(self) -> TStep: + return self.__step.struct() + + def name(self) -> str: + return self.__step.name() + + def type(self) -> str: + return self.__step.type() + + def run(self, runner: HttpRunner) -> StepData: + return self.__step.run(runner) diff --git a/httprunner/step_request.py b/httprunner/step_request.py new file mode 100644 index 00000000..2633bcbd --- /dev/null +++ b/httprunner/step_request.py @@ -0,0 +1,462 @@ +import time +from typing import Any, Dict, List, Text, Union + +from loguru import logger + +from httprunner import utils +from httprunner.exceptions import ValidationFailure +from httprunner.ext.uploader import prepare_upload_step +from httprunner.models import (Hooks, IStep, MethodEnum, StepData, TRequest, + TStep, VariablesMapping) +from httprunner.parser import build_url +from httprunner.response import ResponseObject +from httprunner.runner import HttpRunner + + +def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text): + """ call hook actions. + + Args: + hooks (list): each hook in hooks list maybe in two format. + + format1 (str): only call hook functions. + ${func()} + format2 (dict): assignment, the value returned by hook function will be assigned to variable. + {"var": "${func()}"} + + step_variables: current step variables to call hook, include two special variables + + request: parsed request dict + response: ResponseObject for current response + + hook_msg: setup/teardown request/testcase + + """ + logger.info(f"call hook actions: {hook_msg}") + + if not isinstance(hooks, List): + logger.error(f"Invalid hooks format: {hooks}") + return + + for hook in hooks: + if isinstance(hook, Text): + # format 1: ["${func()}"] + logger.debug(f"call hook function: {hook}") + runner.parser.parse_data(hook, step_variables) + elif isinstance(hook, Dict) and len(hook) == 1: + # format 2: {"var": "${func()}"} + var_name, hook_content = list(hook.items())[0] + hook_content_eval = runner.parser.parse_data( + hook_content, step_variables + ) + logger.debug( + f"call hook function: {hook_content}, got value: {hook_content_eval}" + ) + logger.debug(f"assign variable: {var_name} = {hook_content_eval}") + step_variables[var_name] = hook_content_eval + else: + logger.error(f"Invalid hook format: {hook}") + + +def run_step_request(runner: HttpRunner, step: TStep) -> StepData: + """run teststep: request""" + step_data = StepData( + name=step.name, + success=False, + ) + + step.variables = runner.merge_step_variables(step.variables) + + # parse + functions = runner.parser.functions_mapping + prepare_upload_step(step, functions) + request_dict = step.request.dict() + request_dict.pop("upload", None) + parsed_request_dict = runner.parser.parse_data( + request_dict, step.variables + ) + parsed_request_dict["headers"].setdefault( + "HRUN-Request-ID", + f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}", + ) + step.variables["request"] = parsed_request_dict + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step.variables, "setup request") + + # prepare arguments + config = runner.get_config() + method = parsed_request_dict.pop("method") + url_path = parsed_request_dict.pop("url") + url = build_url(config.base_url, url_path) + parsed_request_dict["verify"] = config.verify + parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) + + # request + resp = runner.session.request(method, url, **parsed_request_dict) + resp_obj = ResponseObject(resp, runner.parser) + step.variables["response"] = resp_obj + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step.variables, "teardown request") + + def log_req_resp_details(): + err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32) + + # log request + err_msg += "====== request details ======\n" + err_msg += f"url: {url}\n" + err_msg += f"method: {method}\n" + headers = parsed_request_dict.pop("headers", {}) + err_msg += f"headers: {headers}\n" + for k, v in parsed_request_dict.items(): + v = utils.omit_long_data(v) + err_msg += f"{k}: {repr(v)}\n" + + err_msg += "\n" + + # log response + err_msg += "====== response details ======\n" + err_msg += f"status_code: {resp.status_code}\n" + err_msg += f"headers: {resp.headers}\n" + err_msg += f"body: {repr(resp.text)}\n" + logger.error(err_msg) + + # extract + extractors = step.extract + extract_mapping = resp_obj.extract(extractors, step.variables) + step_data.export_vars = extract_mapping + + variables_mapping = step.variables + variables_mapping.update(extract_mapping) + + # validate + validators = step.validators + try: + resp_obj.validate( + validators, variables_mapping + ) + step_data.success = True + except ValidationFailure: + log_req_resp_details() + # log testcase duration before raise ValidationFailure + step_data.elapsed = time.time() - runner.__start_at + raise + finally: + session_data = runner.session.data + session_data.success = step_data.success + session_data.validators = resp_obj.validation_results + + # save step data + step_data.data = session_data + + return step_data + + +class StepRequestValidation(IStep): + def __init__(self, step: TStep): + self.__step = step + + def assert_equal( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"equal": [jmes_path, expected_value, message]} + ) + return self + + def assert_not_equal( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"not_equal": [jmes_path, expected_value, message]} + ) + return self + + def assert_greater_than( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"greater_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_less_than( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"less_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_greater_or_equals( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"greater_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_less_or_equals( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"less_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_equal( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_equal": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_greater_than( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_greater_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_less_than( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_less_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_greater_or_equals( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_greater_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_less_or_equals( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_less_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_string_equals( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"string_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_startswith( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"startswith": [jmes_path, expected_value, message]} + ) + return self + + def assert_endswith( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"endswith": [jmes_path, expected_value, message]} + ) + return self + + def assert_regex_match( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"regex_match": [jmes_path, expected_value, message]} + ) + return self + + def assert_contains( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"contains": [jmes_path, expected_value, message]} + ) + return self + + def assert_contained_by( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"contained_by": [jmes_path, expected_value, message]} + ) + return self + + def assert_type_match( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"type_match": [jmes_path, expected_value, message]} + ) + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class StepRequestExtraction(IStep): + def __init__(self, step: TStep): + self.__step = step + + def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction": + self.__step.extract[var_name] = jmes_path + return self + + # def with_regex(self): + # # TODO: extract response html with regex + # pass + # + # def with_jsonpath(self): + # # TODO: extract response json with jsonpath + # pass + + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__step) + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class RequestWithOptionalArgs(IStep): + def __init__(self, step: TStep): + self.__step = step + + def with_params(self, **params) -> "RequestWithOptionalArgs": + self.__step.request.params.update(params) + return self + + def with_headers(self, **headers) -> "RequestWithOptionalArgs": + self.__step.request.headers.update(headers) + return self + + def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": + self.__step.request.cookies.update(cookies) + return self + + def with_data(self, data) -> "RequestWithOptionalArgs": + self.__step.request.data = data + return self + + def with_json(self, req_json) -> "RequestWithOptionalArgs": + self.__step.request.req_json = req_json + return self + + def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": + self.__step.request.timeout = timeout + return self + + def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": + self.__step.request.verify = verify + return self + + def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": + self.__step.request.allow_redirects = allow_redirects + return self + + def upload(self, **file_info) -> "RequestWithOptionalArgs": + self.__step.request.upload.update(file_info) + return self + + def teardown_hook( + self, hook: Text, assign_var_name: Text = None + ) -> "RequestWithOptionalArgs": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def extract(self) -> StepRequestExtraction: + return StepRequestExtraction(self.__step) + + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__step) + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class RunRequest(object): + def __init__(self, name: Text): + self.__step = TStep(name=name) + + def with_variables(self, **variables) -> "RunRequest": + self.__step.variables.update(variables) + 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}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def get(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.GET, url=url) + return RequestWithOptionalArgs(self.__step) + + def post(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.POST, url=url) + return RequestWithOptionalArgs(self.__step) + + def put(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.PUT, url=url) + return RequestWithOptionalArgs(self.__step) + + def head(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.HEAD, url=url) + return RequestWithOptionalArgs(self.__step) + + def delete(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.DELETE, url=url) + return RequestWithOptionalArgs(self.__step) + + def options(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.OPTIONS, url=url) + return RequestWithOptionalArgs(self.__step) + + def patch(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.PATCH, url=url) + return RequestWithOptionalArgs(self.__step) diff --git a/httprunner/step_testcase.py b/httprunner/step_testcase.py new file mode 100644 index 00000000..0882428d --- /dev/null +++ b/httprunner/step_testcase.py @@ -0,0 +1,110 @@ +import os +from typing import Text, Callable + +from loguru import logger +from httprunner import exceptions +from httprunner.loader import load_testcase_file + +from httprunner.step_request import call_hooks +from httprunner.runner import HttpRunner +from httprunner.models import ( + TStep, + StepData +) + + +def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: + """run teststep: referenced testcase""" + step_data = StepData(name=step.name) + step_variables = step.variables + step_export = step.export + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step_variables, "setup testcase") + + # TODO: override testcase with current step name/variables/export + + ref_case_runner = HttpRunner() + ref_case_runner.config = step.testcase.config + ref_case_runner.teststeps = step.testcase.teststeps + ref_case_runner.with_session(runner.session) \ + .with_case_id(runner.case_id) \ + .with_variables(step_variables) \ + .with_export(step_export) \ + .test_start() + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step.variables, "teardown testcase") + + summary = ref_case_runner.get_summary() + step_data.data = summary.step_datas # list of step data + step_data.export_vars = summary.in_out.export_vars + step_data.success = summary.success + + if step_data.export_vars: + logger.info(f"export variables: {step_data.export_vars}") + + return step_data + + +class StepRefCase(object): + def __init__(self, step: TStep): + self.__step = step + + def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def export(self, *var_name: Text) -> "StepRefCase": + self.__step.export.extend(var_name) + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_testcase(runner, self.__step) + + +class RunTestCase(object): + def __init__(self, name: Text): + self.__step = TStep(name=name) + + def with_variables(self, **variables) -> "RunTestCase": + self.__step.variables.update(variables) + 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}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def call(self, testcase: Callable) -> StepRefCase: + if hasattr(testcase, "config") and hasattr(testcase, "teststeps"): + self.__step.testcase = testcase + elif isinstance(testcase, Text): + if not os.path.isfile(testcase): + raise exceptions.ParamsError(f"Invalid testcase path: {testcase}") + + self.__step.testcase = load_testcase_file(testcase) + else: + raise exceptions.ParamsError( + f"Invalid teststep referenced testcase: {testcase}" + ) + + return StepRefCase(self.__step) diff --git a/httprunner/testcase.py b/httprunner/testcase.py deleted file mode 100644 index 86d1554f..00000000 --- a/httprunner/testcase.py +++ /dev/null @@ -1,415 +0,0 @@ -import inspect -from typing import Text, Any, Union, Callable - -from httprunner.models import ( - TConfig, - TStep, - TRequest, - MethodEnum, - TestCase, -) - - -class Config(object): - def __init__(self, name: Text): - self.__name = name - self.__variables = {} - self.__base_url = "" - self.__verify = False - self.__export = [] - self.__weight = 1 - - caller_frame = inspect.stack()[1] - self.__path = caller_frame.filename - - @property - def name(self) -> Text: - return self.__name - - @property - def path(self) -> Text: - return self.__path - - @property - def weight(self) -> int: - return self.__weight - - def variables(self, **variables) -> "Config": - self.__variables.update(variables) - return self - - def base_url(self, base_url: Text) -> "Config": - self.__base_url = base_url - return self - - def verify(self, verify: bool) -> "Config": - self.__verify = verify - return self - - def export(self, *export_var_name: Text) -> "Config": - self.__export.extend(export_var_name) - return self - - def locust_weight(self, weight: int) -> "Config": - self.__weight = weight - 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, - weight=self.__weight, - ) - - -class StepRequestValidation(object): - def __init__(self, step_context: TStep): - self.__step_context = step_context - - def assert_equal( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"equal": [jmes_path, expected_value, message]} - ) - return self - - def assert_not_equal( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"not_equal": [jmes_path, expected_value, message]} - ) - return self - - def assert_greater_than( - self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"greater_than": [jmes_path, expected_value, message]} - ) - return self - - def assert_less_than( - self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"less_than": [jmes_path, expected_value, message]} - ) - return self - - def assert_greater_or_equals( - self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"greater_or_equals": [jmes_path, expected_value, message]} - ) - return self - - def assert_less_or_equals( - self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"less_or_equals": [jmes_path, expected_value, message]} - ) - return self - - def assert_length_equal( - self, jmes_path: Text, expected_value: int, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"length_equal": [jmes_path, expected_value, message]} - ) - return self - - def assert_length_greater_than( - self, jmes_path: Text, expected_value: int, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"length_greater_than": [jmes_path, expected_value, message]} - ) - return self - - def assert_length_less_than( - self, jmes_path: Text, expected_value: int, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"length_less_than": [jmes_path, expected_value, message]} - ) - return self - - def assert_length_greater_or_equals( - self, jmes_path: Text, expected_value: int, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"length_greater_or_equals": [jmes_path, expected_value, message]} - ) - return self - - def assert_length_less_or_equals( - self, jmes_path: Text, expected_value: int, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"length_less_or_equals": [jmes_path, expected_value, message]} - ) - return self - - def assert_string_equals( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"string_equals": [jmes_path, expected_value, message]} - ) - return self - - def assert_startswith( - self, jmes_path: Text, expected_value: Text, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"startswith": [jmes_path, expected_value, message]} - ) - return self - - def assert_endswith( - self, jmes_path: Text, expected_value: Text, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"endswith": [jmes_path, expected_value, message]} - ) - return self - - def assert_regex_match( - self, jmes_path: Text, expected_value: Text, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"regex_match": [jmes_path, expected_value, message]} - ) - return self - - def assert_contains( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"contains": [jmes_path, expected_value, message]} - ) - return self - - def assert_contained_by( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"contained_by": [jmes_path, expected_value, message]} - ) - return self - - def assert_type_match( - self, jmes_path: Text, expected_value: Any, message: Text = "" - ) -> "StepRequestValidation": - self.__step_context.validators.append( - {"type_match": [jmes_path, expected_value, message]} - ) - return self - - def perform(self) -> TStep: - return self.__step_context - - -class StepRequestExtraction(object): - def __init__(self, step_context: TStep): - self.__step_context = step_context - - def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction": - self.__step_context.extract[var_name] = jmes_path - return self - - # def with_regex(self): - # # TODO: extract response html with regex - # pass - # - # def with_jsonpath(self): - # # TODO: extract response json with jsonpath - # pass - - def validate(self) -> StepRequestValidation: - return StepRequestValidation(self.__step_context) - - def perform(self) -> TStep: - return self.__step_context - - -class RequestWithOptionalArgs(object): - def __init__(self, step_context: TStep): - self.__step_context = step_context - - def with_params(self, **params) -> "RequestWithOptionalArgs": - self.__step_context.request.params.update(params) - return self - - def with_headers(self, **headers) -> "RequestWithOptionalArgs": - self.__step_context.request.headers.update(headers) - return self - - def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": - self.__step_context.request.cookies.update(cookies) - return self - - def with_data(self, data) -> "RequestWithOptionalArgs": - self.__step_context.request.data = data - return self - - def with_json(self, req_json) -> "RequestWithOptionalArgs": - self.__step_context.request.req_json = req_json - return self - - def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": - self.__step_context.request.timeout = timeout - return self - - def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": - self.__step_context.request.verify = verify - return self - - def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": - self.__step_context.request.allow_redirects = allow_redirects - return self - - def upload(self, **file_info) -> "RequestWithOptionalArgs": - self.__step_context.request.upload.update(file_info) - return self - - def teardown_hook( - self, hook: Text, assign_var_name: Text = None - ) -> "RequestWithOptionalArgs": - if assign_var_name: - self.__step_context.teardown_hooks.append({assign_var_name: hook}) - else: - self.__step_context.teardown_hooks.append(hook) - - return self - - def extract(self) -> StepRequestExtraction: - return StepRequestExtraction(self.__step_context) - - def validate(self) -> StepRequestValidation: - return StepRequestValidation(self.__step_context) - - def perform(self) -> TStep: - return self.__step_context - - -class RunRequest(object): - def __init__(self, name: Text): - self.__step_context = TStep(name=name) - - def with_variables(self, **variables) -> "RunRequest": - self.__step_context.variables.update(variables) - return self - - def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest": - if assign_var_name: - self.__step_context.setup_hooks.append({assign_var_name: hook}) - else: - self.__step_context.setup_hooks.append(hook) - - return self - - def get(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.GET, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def post(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.POST, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def put(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def head(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def delete(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def options(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url) - return RequestWithOptionalArgs(self.__step_context) - - def patch(self, url: Text) -> RequestWithOptionalArgs: - self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url) - return RequestWithOptionalArgs(self.__step_context) - - -class StepRefCase(object): - def __init__(self, step_context: TStep): - self.__step_context = step_context - - def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase": - if assign_var_name: - self.__step_context.teardown_hooks.append({assign_var_name: hook}) - else: - self.__step_context.teardown_hooks.append(hook) - - return self - - def export(self, *var_name: Text) -> "StepRefCase": - self.__step_context.export.extend(var_name) - return self - - def perform(self) -> TStep: - return self.__step_context - - -class RunTestCase(object): - def __init__(self, name: Text): - self.__step_context = TStep(name=name) - - def with_variables(self, **variables) -> "RunTestCase": - self.__step_context.variables.update(variables) - return self - - def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase": - if assign_var_name: - self.__step_context.setup_hooks.append({assign_var_name: hook}) - else: - self.__step_context.setup_hooks.append(hook) - - return self - - def call(self, testcase: Callable) -> StepRefCase: - self.__step_context.testcase = testcase - return StepRefCase(self.__step_context) - - def perform(self) -> TStep: - return self.__step_context - - -class Step(object): - def __init__( - self, - step_context: Union[ - StepRequestValidation, - StepRequestExtraction, - RequestWithOptionalArgs, - RunTestCase, - StepRefCase, - ], - ): - self.__step_context = step_context.perform() - - @property - def request(self) -> TRequest: - return self.__step_context.request - - @property - def testcase(self) -> TestCase: - return self.__step_context.testcase - - def perform(self) -> TStep: - return self.__step_context From eac548f1e78e51553f34cddf1e7a421426981c32 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 2 Apr 2022 10:28:25 +0800 Subject: [PATCH 03/12] fix: run referenced testcase in python --- 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 +- httprunner/cli.py | 2 +- httprunner/cli_test.py | 14 ++++++++-- httprunner/models.py | 17 +++++------- httprunner/response_test.py | 13 +++++----- httprunner/runner.py | 12 ++++----- httprunner/runner_test.py | 40 ----------------------------- httprunner/step_request_test.py | 16 ++++++++++++ httprunner/step_testcase.py | 28 +++++++------------- httprunner/step_testcase_test.py | 23 +++++++++++++++++ 14 files changed, 85 insertions(+), 90 deletions(-) delete mode 100644 httprunner/runner_test.py create mode 100644 httprunner/step_request_test.py create mode 100644 httprunner/step_testcase_test.py diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py index c16102bb..eec02f1f 100644 --- a/examples/httpbin/basic_test.py +++ b/examples/httpbin/basic_test.py @@ -1,4 +1,4 @@ -# NOTE: Generated By HttpRunner v3.1.7 +# NOTE: Generated By HttpRunner v4.0.0-alpha # FROM: basic.yml diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py index ef4fce08..0a696d8f 100644 --- a/examples/httpbin/hooks_test.py +++ b/examples/httpbin/hooks_test.py @@ -1,4 +1,4 @@ -# NOTE: Generated By HttpRunner v3.1.7 +# NOTE: Generated By HttpRunner v4.0.0-alpha # FROM: hooks.yml diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py index a3827b85..d00e0e22 100644 --- a/examples/httpbin/load_image_test.py +++ b/examples/httpbin/load_image_test.py @@ -1,4 +1,4 @@ -# NOTE: Generated By HttpRunner v3.1.7 +# NOTE: Generated By HttpRunner v4.0.0-alpha # FROM: load_image.yml diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py index c3886e0c..f9ef8406 100644 --- a/examples/httpbin/upload_test.py +++ b/examples/httpbin/upload_test.py @@ -1,4 +1,4 @@ -# NOTE: Generated By HttpRunner v3.1.7 +# NOTE: Generated By HttpRunner v4.0.0-alpha # FROM: upload.yml diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py index c6ddd3f9..fe1c2b76 100644 --- a/examples/httpbin/validate_test.py +++ b/examples/httpbin/validate_test.py @@ -1,4 +1,4 @@ -# NOTE: Generated By HttpRunner v3.1.7 +# NOTE: Generated By HttpRunner v4.0.0-alpha # FROM: validate.yml diff --git a/httprunner/cli.py b/httprunner/cli.py index d015313e..38676857 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -63,7 +63,7 @@ def main(): ) subparsers = parser.add_subparsers(help="sub-command help") - sub_parser_run = init_parser_run(subparsers) + init_parser_run(subparsers) sub_parser_make = init_make_parser(subparsers) if len(sys.argv) == 1: diff --git a/httprunner/cli_test.py b/httprunner/cli_test.py index 863ef0bd..8a95ef0f 100644 --- a/httprunner/cli_test.py +++ b/httprunner/cli_test.py @@ -5,7 +5,8 @@ import unittest import pytest -from httprunner.cli import main +from httprunner import loader +from httprunner.cli import main, main_run class TestCli(unittest.TestCase): @@ -45,8 +46,17 @@ class TestCli(unittest.TestCase): try: os.chdir(os.path.join(cwd, "examples", "postman_echo")) exit_code = pytest.main( - ["-s", "request_methods/request_with_testcase_reference_test.py",] + ["-s", "request_methods/request_with_testcase_reference_test.py"] ) self.assertEqual(exit_code, 0) finally: os.chdir(cwd) + + def test_run_testcase_with_abnormal_path(self): + loader.project_meta = None + exit_code = main_run(["examples/data/a-b.c/2 3.yml"]) + self.assertEqual(exit_code, 0) + self.assertTrue(os.path.exists("examples/data/a_b_c/__init__.py")) + self.assertTrue(os.path.exists("examples/data/debugtalk.py")) + self.assertTrue(os.path.exists("examples/data/a_b_c/T1_test.py")) + self.assertTrue(os.path.exists("examples/data/a_b_c/T2_3_test.py")) diff --git a/httprunner/models.py b/httprunner/models.py index 95d2fb30..bff6a0c7 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -1,11 +1,8 @@ import os from enum import Enum -from typing import Any -from typing import Dict, Text, Union, Callable -from typing import List +from typing import Any, Callable, Dict, List, Text, Union -from pydantic import BaseModel, Field -from pydantic import HttpUrl +from pydantic import BaseModel, Field, HttpUrl Name = Text Url = Text @@ -156,14 +153,14 @@ class SessionData(BaseModel): class StepData(BaseModel): """teststep data, each step maybe corresponding to one request or one testcase""" - name: Text = "" # teststep name - step_type: Text = "" # teststep type, request or testcase + name: Text = "" # teststep name + step_type: Text = "" # teststep type, request or testcase success: bool = False data: Union[SessionData, List['StepData']] = None - elapsed: float = 0.0 # teststep elapsed time - content_size: float = 0 # response content size + elapsed: float = 0.0 # teststep elapsed time + content_size: float = 0 # response content size export_vars: VariablesMapping = {} - attachment: Text = "" # teststep attachment + attachment: Text = "" # teststep attachment StepData.update_forward_refs() diff --git a/httprunner/response_test.py b/httprunner/response_test.py index 05e9cc39..8b292f46 100644 --- a/httprunner/response_test.py +++ b/httprunner/response_test.py @@ -2,6 +2,7 @@ import unittest import requests +from httprunner.parser import Parser from httprunner.response import ResponseObject @@ -18,15 +19,16 @@ class TestResponse(unittest.TestCase): ] }, ) - self.resp_obj = ResponseObject(resp) + parser = Parser(functions_mapping={ + 'get_name': lambda: 'name', + "get_num": lambda x: x + }) + self.resp_obj = ResponseObject(resp, parser) def test_extract(self): variables_mapping = { 'body': 'body' } - functions_mapping = { - 'get_name': lambda: 'name', - } extract_mapping = self.resp_obj.extract( { "var_1": "body.json.locations[0]", @@ -35,7 +37,6 @@ class TestResponse(unittest.TestCase): "var_4": "$body.json.locations[3].${get_name()}", }, variables_mapping=variables_mapping, - functions_mapping=functions_mapping, ) self.assertEqual(extract_mapping["var_1"], {"name": "Seattle", "state": "WA"}) self.assertEqual(extract_mapping["var_2"], "Olympia") @@ -62,9 +63,7 @@ class TestResponse(unittest.TestCase): def test_validate_functions(self): variables_mapping = {"index": 1} - functions_mapping = {"get_num": lambda x: x} self.resp_obj.validate( [{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},], variables_mapping=variables_mapping, - functions_mapping=functions_mapping, ) diff --git a/httprunner/runner.py b/httprunner/runner.py index af9b6ee7..24b40d0c 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -16,9 +16,10 @@ from loguru import logger from httprunner.client import HttpSession from httprunner.config import Config from httprunner.exceptions import ParamsError -from httprunner.loader import load_project_meta -from httprunner.models import (ProjectMeta, StepData, TConfig, TestCaseInOut, - TestCaseSummary, TestCaseTime, VariablesMapping) +from httprunner.loader import load_project_meta, load_testcase_file +from httprunner.models import (ProjectMeta, StepData, TConfig, TestCase, + TestCaseInOut, TestCaseSummary, TestCaseTime, + VariablesMapping) from httprunner.parser import Parser from httprunner.utils import merge_variables @@ -175,11 +176,9 @@ class HttpRunner(object): logger.info(f"run step end: {step.name()} <<<<<<\n") - def test_start(self, param: Dict = None): + def test_start(self, param: Dict = None) -> "HttpRunner": """main entrance, discovered by pytest""" self.__init() - log_handler = logger.add(self.__log_path, level="DEBUG") - self.__parse_config(param) if USE_ALLURE: @@ -191,6 +190,7 @@ class HttpRunner(object): f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}" ) + log_handler = logger.add(self.__log_path, level="DEBUG") self.__start_at = time.time() try: # run step in sequential order diff --git a/httprunner/runner_test.py b/httprunner/runner_test.py deleted file mode 100644 index ef59dcdf..00000000 --- a/httprunner/runner_test.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import unittest - -from httprunner import loader -from httprunner.cli import main_run -from httprunner.runner import HttpRunner - - -class TestHttpRunner(unittest.TestCase): - def setUp(self): - loader.project_meta = None - self.runner = HttpRunner() - - def test_run_testcase_by_path_request_only(self): - self.runner.run_path( - "examples/postman_echo/request_methods/request_with_functions.yml" - ) - result = self.runner.get_summary() - self.assertTrue(result.success) - self.assertEqual(result.name, "request methods testcase with functions") - self.assertEqual(result.step_datas[0].name, "get with params") - self.assertEqual(len(result.step_datas), 3) - - def test_run_testcase_by_path_ref_testcase(self): - self.runner.run_path( - "examples/postman_echo/request_methods/request_with_testcase_reference.yml" - ) - result = self.runner.get_summary() - 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), 2) - - def test_run_testcase_with_abnormal_path(self): - exit_code = main_run(["examples/data/a-b.c/2 3.yml"]) - self.assertEqual(exit_code, 0) - self.assertTrue(os.path.exists("examples/data/a_b_c/__init__.py")) - self.assertTrue(os.path.exists("examples/data/debugtalk.py")) - self.assertTrue(os.path.exists("examples/data/a_b_c/T1_test.py")) - self.assertTrue(os.path.exists("examples/data/a_b_c/T2_3_test.py")) diff --git a/httprunner/step_request_test.py b/httprunner/step_request_test.py new file mode 100644 index 00000000..f6984b78 --- /dev/null +++ b/httprunner/step_request_test.py @@ -0,0 +1,16 @@ +import unittest + +from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions + + +class TestRunRequest(unittest.TestCase): + + def test_run_request(self): + runner = TestCaseRequestWithFunctions().test_start() + summary = runner.get_summary() + self.assertTrue(summary.success) + self.assertEqual(summary.name, "request methods testcase with functions") + self.assertEqual(len(summary.step_datas), 3) + self.assertEqual(summary.step_datas[0].name, "get with params") + self.assertEqual(summary.step_datas[1].name, "post raw text") + self.assertEqual(summary.step_datas[2].name, "post form data") diff --git a/httprunner/step_testcase.py b/httprunner/step_testcase.py index 0882428d..337ab21f 100644 --- a/httprunner/step_testcase.py +++ b/httprunner/step_testcase.py @@ -1,16 +1,11 @@ -import os -from typing import Text, Callable +from typing import Callable, Text from loguru import logger -from httprunner import exceptions -from httprunner.loader import load_testcase_file -from httprunner.step_request import call_hooks +from httprunner import exceptions +from httprunner.models import IStep, StepData, TStep from httprunner.runner import HttpRunner -from httprunner.models import ( - TStep, - StepData -) +from httprunner.step_request import call_hooks def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: @@ -25,9 +20,8 @@ def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: # TODO: override testcase with current step name/variables/export - ref_case_runner = HttpRunner() - ref_case_runner.config = step.testcase.config - ref_case_runner.teststeps = step.testcase.teststeps + # step.testcase is a referenced testcase, e.g. RequestWithFunctions + ref_case_runner = step.testcase() ref_case_runner.with_session(runner.session) \ .with_case_id(runner.case_id) \ .with_variables(step_variables) \ @@ -49,7 +43,7 @@ def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: return step_data -class StepRefCase(object): +class StepRefCase(IStep): def __init__(self, step: TStep): self.__step = step @@ -95,13 +89,9 @@ class RunTestCase(object): return self def call(self, testcase: Callable) -> StepRefCase: - if hasattr(testcase, "config") and hasattr(testcase, "teststeps"): + if issubclass(testcase, HttpRunner): + # referenced testcase object self.__step.testcase = testcase - elif isinstance(testcase, Text): - if not os.path.isfile(testcase): - raise exceptions.ParamsError(f"Invalid testcase path: {testcase}") - - self.__step.testcase = load_testcase_file(testcase) else: raise exceptions.ParamsError( f"Invalid teststep referenced testcase: {testcase}" diff --git a/httprunner/step_testcase_test.py b/httprunner/step_testcase_test.py new file mode 100644 index 00000000..926e8897 --- /dev/null +++ b/httprunner/step_testcase_test.py @@ -0,0 +1,23 @@ +import unittest + +from httprunner.runner import HttpRunner +from httprunner.step_testcase import RunTestCase +from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions + + +class TestRunTestCase(unittest.TestCase): + + def setUp(self): + self.runner = HttpRunner() + + def test_run_testcase_by_path(self): + + step_data = RunTestCase("run referenced testcase").call( + TestCaseRequestWithFunctions + ).run(self.runner) + self.assertTrue(step_data.success) + self.assertEqual(step_data.name, "run referenced testcase") + self.assertEqual(len(step_data.data), 3) + self.assertEqual(step_data.data[0].name, "get with params") + self.assertEqual(step_data.data[1].name, "post raw text") + self.assertEqual(step_data.data[2].name, "post form data") From 1b70678bfd3de71ef5f6e57a4b31a6e8f3646617 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 2 Apr 2022 12:12:42 +0800 Subject: [PATCH 04/12] docs: add docs --- hrp/README.md | 15 +++--- hrp/session.go | 10 ++-- httprunner/README.md | 115 +++++++++++++++++++++++++++++++++++++++++++ httprunner/runner.py | 24 +++++---- 4 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 httprunner/README.md diff --git a/hrp/README.md b/hrp/README.md index e0d63e7a..31878c5f 100644 --- a/hrp/README.md +++ b/hrp/README.md @@ -1,3 +1,4 @@ +# 代码阅读指南(golang 部分) ## 核心数据结构 @@ -21,14 +22,14 @@ type IStep interface { } ``` -我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前已支持的步骤类型包括: +我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 hrp 已支持的步骤类型包括: -- request:发起单次 HTTP 请求 -- api:引用执行其它 API 文件 -- testcase:引用执行其它测试用例文件 -- thinktime:思考时间,按照配置的逻辑进行等待 -- transaction:事务机制,用于压测 -- rendezvous:集合点机制,用于压测 +- [request](step_request.go):发起单次 HTTP 请求 +- [api](step_api.go):引用执行其它 API 文件 +- [testcase](step_testcase.go):引用执行其它测试用例文件 +- [thinktime](step_thinktime.go):思考时间,按照配置的逻辑进行等待 +- [transaction](step_transaction.go):事务机制,用于压测 +- [rendezvous](step_rendezvous.go):集合点机制,用于压测 基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。 diff --git a/hrp/session.go b/hrp/session.go index 78674bf2..51966bb0 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -83,7 +83,9 @@ func (r *SessionRunner) Start() error { } // update extracted variables - r.updateSession(stepResult.ExportVars) + for k, v := range stepResult.ExportVars { + r.sessionVariables[k] = v + } // update testcase summary r.updateSummary(stepResult) @@ -99,12 +101,6 @@ func (r *SessionRunner) Start() error { return nil } -func (r *SessionRunner) updateSession(vars map[string]interface{}) { - for k, v := range vars { - r.sessionVariables[k] = v - } -} - // updateSummary appends step result to summary func (r *SessionRunner) updateSummary(stepResult *StepResult) { r.summary.Records = append(r.summary.Records, stepResult) diff --git a/httprunner/README.md b/httprunner/README.md new file mode 100644 index 00000000..c6d241b2 --- /dev/null +++ b/httprunner/README.md @@ -0,0 +1,115 @@ +# 代码阅读指南(python 部分) + +## 核心数据结构 + +HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤的集合。 + +```py +class TestCase(BaseModel): + config: TConfig + teststeps: List[TStep] +``` + +针对每种测试步骤,统一继承自 `IStep`,并要求必须至少实现如下 4 个方法;步骤内容统一在 `run` 方法中进行实现。 + +```py +class IStep(object): + + def name(self) -> str: + raise NotImplementedError + + def type(self) -> str: + raise NotImplementedError + + def struct(self) -> TStep: + raise NotImplementedError + + def run(self, runner) -> StepData: + # runner: HttpRunner + raise NotImplementedError +``` + +我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 python 版本已支持的步骤类型包括: + +- [request](step_request.py):发起单次 HTTP 请求 +- [testcase](step_testcase.py):引用执行其它测试用例文件 + +基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。 + +## 用例编写 + +## 运行主流程 + +### 整体控制器 pytest + +不同于 golang 版本,python 版本的控制逻辑都基于 `pytest` 的用例发现和执行机制。 + +- 如果是运行 JSON/YAML 格式的用例,hrp 会将用例转换为 pytest 支持的用例格式 +- 如果是要自行编写 pytest 测试用例,需要遵循 HttpRunner 的格式要求 + +### pytest 用例格式要求 + +所有测试用例要求都继承自 `HttpRunner`,然后 + +结构如下所示: + +```py +class TestCaseRequestWithFunctions(HttpRunner): + + config = ( + Config("request methods testcase with functions") + ) + + teststeps = [ + Step( + RunRequest("get with params")... + ), + Step( + RunRequest("post raw text")... + ), + Step( + RunRequest("post form data")... + ), + ] +``` + +完整案例可参考: + +- [request_with_functions_test.py](../examples/postman_echo/request_methods/request_with_functions_test.py):用例中包含了 requests 的情况 +- [request_with_testcase_reference_test.py](../examples/postman_echo/request_methods/request_with_testcase_reference_test.py):用例中包含了引用其它测试用例的情况 + +### 用例执行器 SessionRunner + +测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 + +```py +class SessionRunner(object): + config: Config + teststeps: List[object] # list of Step + ... +``` + +重点关注一个方法: + +- test_start:该方法将被 pytest 发现,作为启动执行入口,依次执行所有测试步骤 + +```go +def test_start(self, param: Dict = None) -> "SessionRunner": + """main entrance, discovered by pytest""" + self.__start_at = time.time() + try: + # run step in sequential order + for step in self.teststeps: + self.__run_step(step) + finally: + logger.info(f"generate testcase log: {self.__log_path}") + + self.__duration = time.time() - self.__start_at +``` + +在主流程中,SessionRunner 并不需要关注 step 的具体类型,统一都是调用 `step.run(self)`,具体实现逻辑都在对应 step 的 `run` 方法中。 + +```py +def run(self, runner: HttpRunner) -> StepData: + return self.__step.run(runner) +``` diff --git a/httprunner/runner.py b/httprunner/runner.py index 24b40d0c..6100692a 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -16,15 +16,14 @@ from loguru import logger from httprunner.client import HttpSession from httprunner.config import Config from httprunner.exceptions import ParamsError -from httprunner.loader import load_project_meta, load_testcase_file -from httprunner.models import (ProjectMeta, StepData, TConfig, TestCase, - TestCaseInOut, TestCaseSummary, TestCaseTime, - VariablesMapping) +from httprunner.loader import load_project_meta +from httprunner.models import (ProjectMeta, StepData, TConfig, TestCaseInOut, + TestCaseSummary, TestCaseTime, VariablesMapping) from httprunner.parser import Parser from httprunner.utils import merge_variables -class HttpRunner(object): +class SessionRunner(object): config: Config teststeps: List[object] # list of Step @@ -63,22 +62,22 @@ class HttpRunner(object): self.session = self.session or HttpSession() self.parser = self.parser or Parser(self.__project_meta.functions) - def with_session(self, session: HttpSession) -> "HttpRunner": + def with_session(self, session: HttpSession) -> "SessionRunner": self.session = session return self def get_config(self) -> TConfig: return self.__config - def with_case_id(self, case_id: Text) -> "HttpRunner": + def with_case_id(self, case_id: Text) -> "SessionRunner": self.case_id = case_id return self - def with_variables(self, variables: VariablesMapping) -> "HttpRunner": + def with_variables(self, variables: VariablesMapping) -> "SessionRunner": self.__session_variables = variables return self - def with_export(self, export: List[Text]) -> "HttpRunner": + def with_export(self, export: List[Text]) -> "SessionRunner": self.__export = export return self @@ -176,7 +175,7 @@ class HttpRunner(object): logger.info(f"run step end: {step.name()} <<<<<<\n") - def test_start(self, param: Dict = None) -> "HttpRunner": + def test_start(self, param: Dict = None) -> "SessionRunner": """main entrance, discovered by pytest""" self.__init() self.__parse_config(param) @@ -202,3 +201,8 @@ class HttpRunner(object): self.__duration = time.time() - self.__start_at return self + + +class HttpRunner(SessionRunner): + # split SessionRunner to keep consistant with golang version + pass From fec364153669eb6787c5eee9317470b39ab156d4 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sat, 2 Apr 2022 23:25:34 +0800 Subject: [PATCH 05/12] change: rename StepData to StepResult --- examples/postman_echo/conftest.py | 18 +++++++++------- .../postman_echo/request_methods/conftest.py | 2 +- httprunner/compat.py | 14 ++++++------- httprunner/models.py | 10 ++++----- httprunner/runner.py | 20 +++++++++--------- httprunner/step.py | 4 ++-- httprunner/step_request.py | 21 ++++++++++--------- httprunner/step_request_test.py | 8 +++---- httprunner/step_testcase.py | 20 +++++++++--------- httprunner/step_testcase_test.py | 14 ++++++------- 10 files changed, 67 insertions(+), 64 deletions(-) diff --git a/examples/postman_echo/conftest.py b/examples/postman_echo/conftest.py index b55e5414..88a30859 100644 --- a/examples/postman_echo/conftest.py +++ b/examples/postman_echo/conftest.py @@ -12,13 +12,13 @@ from httprunner.utils import get_platform, ExtendJSONEncoder @pytest.fixture(scope="session", autouse=True) def session_fixture(request): """setup and teardown each task""" - logger.info(f"start running testcases ...") + logger.info("start running testcases ...") start_at = time.time() yield - logger.info(f"task finished, generate task summary for --save-tests") + logger.info("task finished, generate task summary for --save-tests") summary = { "success": True, @@ -36,24 +36,27 @@ def session_fixture(request): summary["success"] &= testcase_summary.success summary["stat"]["testcases"]["total"] += 1 - summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) + summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results) if testcase_summary.success: summary["stat"]["testcases"]["success"] += 1 summary["stat"]["teststeps"]["successes"] += len( - testcase_summary.step_datas + testcase_summary.step_results ) else: summary["stat"]["testcases"]["fail"] += 1 summary["stat"]["teststeps"]["successes"] += ( - len(testcase_summary.step_datas) - 1 + len(testcase_summary.step_results) - 1 ) summary["stat"]["teststeps"]["failures"] += 1 testcase_summary_json = testcase_summary.dict() - testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") + testcase_summary_json["records"] = testcase_summary_json.pop("step_results") summary["details"].append(testcase_summary_json) - summary_path = r"/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/postman_echo/logs/request_methods/hardcode.summary.json" + summary_path = os.path.join( + os.getcwd(), + "examples/postman_echo/logs/request_methods/hardcode.summary.json" + ) summary_dir = os.path.dirname(summary_path) os.makedirs(summary_dir, exist_ok=True) @@ -61,4 +64,3 @@ def session_fixture(request): json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder) logger.info(f"generated task summary: {summary_path}") - diff --git a/examples/postman_echo/request_methods/conftest.py b/examples/postman_echo/request_methods/conftest.py index 788c2686..9d872f13 100644 --- a/examples/postman_echo/request_methods/conftest.py +++ b/examples/postman_echo/request_methods/conftest.py @@ -23,7 +23,7 @@ def session_fixture(request): yield - logger.debug(f"teardown task fixture") + logger.debug("teardown task fixture") # teardown task # TODO: upload task summary diff --git a/httprunner/compat.py b/httprunner/compat.py index c14c9d1f..44fc7d7f 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -257,12 +257,12 @@ def ensure_cli_args(args: List) -> List: """ # remove deprecated --failfast if "--failfast" in args: - logger.warning(f"remove deprecated argument: --failfast") + logger.warning("remove deprecated argument: --failfast") args.pop(args.index("--failfast")) # convert --report-file to --html if "--report-file" in args: - logger.warning(f"replace deprecated argument --report-file with --html") + logger.warning("replace deprecated argument --report-file with --html") index = args.index("--report-file") args[index] = "--html" args.append("--self-contained-html") @@ -270,7 +270,7 @@ def ensure_cli_args(args: List) -> List: # keep compatibility with --save-tests in v2 if "--save-tests" in args: logger.warning( - f"generate conftest.py keep compatibility with --save-tests in v2" + "generate conftest.py keep compatibility with --save-tests in v2" ) args.pop(args.index("--save-tests")) _generate_conftest_for_summary(args) @@ -327,21 +327,21 @@ def session_fixture(request): summary["success"] &= testcase_summary.success summary["stat"]["testcases"]["total"] += 1 - summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) + summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results) if testcase_summary.success: summary["stat"]["testcases"]["success"] += 1 summary["stat"]["teststeps"]["successes"] += len( - testcase_summary.step_datas + testcase_summary.step_results ) else: summary["stat"]["testcases"]["fail"] += 1 summary["stat"]["teststeps"]["successes"] += ( - len(testcase_summary.step_datas) - 1 + len(testcase_summary.step_results) - 1 ) summary["stat"]["teststeps"]["failures"] += 1 testcase_summary_json = testcase_summary.dict() - testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") + testcase_summary_json["records"] = testcase_summary_json.pop("step_results") summary["details"].append(testcase_summary_json) summary_path = r"{{SUMMARY_PATH_PLACEHOLDER}}" diff --git a/httprunner/models.py b/httprunner/models.py index bff6a0c7..9ab903ba 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -150,20 +150,20 @@ class SessionData(BaseModel): validators: Dict = {} -class StepData(BaseModel): +class StepResult(BaseModel): """teststep data, each step maybe corresponding to one request or one testcase""" name: Text = "" # teststep name step_type: Text = "" # teststep type, request or testcase success: bool = False - data: Union[SessionData, List['StepData']] = None + data: Union[SessionData, List['StepResult']] = None elapsed: float = 0.0 # teststep elapsed time content_size: float = 0 # response content size export_vars: VariablesMapping = {} attachment: Text = "" # teststep attachment -StepData.update_forward_refs() +StepResult.update_forward_refs() class IStep(object): @@ -177,7 +177,7 @@ class IStep(object): def struct(self) -> TStep: raise NotImplementedError - def run(self, runner) -> StepData: + def run(self, runner) -> StepResult: # runner: HttpRunner raise NotImplementedError @@ -189,7 +189,7 @@ class TestCaseSummary(BaseModel): time: TestCaseTime in_out: TestCaseInOut = {} log: Text = "" - step_datas: List[StepData] = [] + step_results: List[StepResult] = [] class PlatformInfo(BaseModel): diff --git a/httprunner/runner.py b/httprunner/runner.py index 6100692a..4c90bb9b 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -17,7 +17,7 @@ from httprunner.client import HttpSession from httprunner.config import Config from httprunner.exceptions import ParamsError from httprunner.loader import load_project_meta -from httprunner.models import (ProjectMeta, StepData, TConfig, TestCaseInOut, +from httprunner.models import (ProjectMeta, StepResult, TConfig, TestCaseInOut, TestCaseSummary, TestCaseTime, VariablesMapping) from httprunner.parser import Parser from httprunner.utils import merge_variables @@ -35,7 +35,7 @@ class SessionRunner(object): __config: TConfig __project_meta: ProjectMeta = None __export: List[Text] = [] - __step_datas: List[StepData] = [] + __step_results: List[StepResult] = [] __session_variables: VariablesMapping = {} # time __start_at: float = 0 @@ -58,7 +58,7 @@ class SessionRunner(object): self.root_dir, "logs", f"{self.case_id}.run.log" ) - self.__step_datas.clear() + self.__step_results.clear() self.session = self.session or HttpSession() self.parser = self.parser or Parser(self.__project_meta.functions) @@ -120,8 +120,8 @@ class SessionRunner(object): start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat() summary_success = True - for step_data in self.__step_datas: - if not step_data.success: + for step_result in self.__step_results: + if not step_result.success: summary_success = False break @@ -139,7 +139,7 @@ class SessionRunner(object): export_vars=self.get_export_variables(), ), log=self.__log_path, - step_datas=self.__step_datas, + step_results=self.__step_results, ) def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping: @@ -152,7 +152,7 @@ class SessionRunner(object): # parse variables return self.parser.parse_variables(variables) - def __run_step(self, step) -> Dict: + def __run_step(self, step): """run teststep, step maybe any kind that implements IStep interface Args: @@ -164,14 +164,14 @@ class SessionRunner(object): # run step if USE_ALLURE: with allure.step(f"step: {step.name()}"): - step_result = step.run(self) + step_result: StepResult = step.run(self) else: - step_result = step.run(self) + step_result: StepResult = step.run(self) # save extracted variables to session variables self.__session_variables.update(step_result.export_vars) # update testcase summary - self.__step_datas.append(step_result) + self.__step_results.append(step_result) logger.info(f"run step end: {step.name()} <<<<<<\n") diff --git a/httprunner/step.py b/httprunner/step.py index f6bc7c90..f9721122 100644 --- a/httprunner/step.py +++ b/httprunner/step.py @@ -1,6 +1,6 @@ from typing import Union -from httprunner.models import StepData, TRequest, TStep, TestCase +from httprunner.models import StepResult, TRequest, TStep, TestCase from httprunner.runner import HttpRunner from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation from httprunner.step_testcase import StepRefCase @@ -36,5 +36,5 @@ class Step(object): def type(self) -> str: return self.__step.type() - def run(self, runner: HttpRunner) -> StepData: + def run(self, runner: HttpRunner) -> StepResult: return self.__step.run(runner) diff --git a/httprunner/step_request.py b/httprunner/step_request.py index 2633bcbd..419d7bab 100644 --- a/httprunner/step_request.py +++ b/httprunner/step_request.py @@ -6,7 +6,7 @@ from loguru import logger from httprunner import utils from httprunner.exceptions import ValidationFailure from httprunner.ext.uploader import prepare_upload_step -from httprunner.models import (Hooks, IStep, MethodEnum, StepData, TRequest, +from httprunner.models import (Hooks, IStep, MethodEnum, StepResult, TRequest, TStep, VariablesMapping) from httprunner.parser import build_url from httprunner.response import ResponseObject @@ -58,12 +58,13 @@ def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMappin logger.error(f"Invalid hook format: {hook}") -def run_step_request(runner: HttpRunner, step: TStep) -> StepData: +def run_step_request(runner: HttpRunner, step: TStep) -> StepResult: """run teststep: request""" - step_data = StepData( + step_result = StepResult( name=step.name, success=False, ) + start_time = time.time() step.variables = runner.merge_step_variables(step.variables) @@ -127,7 +128,7 @@ def run_step_request(runner: HttpRunner, step: TStep) -> StepData: # extract extractors = step.extract extract_mapping = resp_obj.extract(extractors, step.variables) - step_data.export_vars = extract_mapping + step_result.export_vars = extract_mapping variables_mapping = step.variables variables_mapping.update(extract_mapping) @@ -138,21 +139,20 @@ def run_step_request(runner: HttpRunner, step: TStep) -> StepData: resp_obj.validate( validators, variables_mapping ) - step_data.success = True + step_result.success = True except ValidationFailure: log_req_resp_details() - # log testcase duration before raise ValidationFailure - step_data.elapsed = time.time() - runner.__start_at raise finally: session_data = runner.session.data - session_data.success = step_data.success + session_data.success = step_result.success session_data.validators = resp_obj.validation_results # save step data - step_data.data = session_data + step_result.data = session_data + step_result.elapsed = time.time() - start_time - return step_data + return step_result class StepRequestValidation(IStep): @@ -418,6 +418,7 @@ class RequestWithOptionalArgs(IStep): class RunRequest(object): + def __init__(self, name: Text): self.__step = TStep(name=name) diff --git a/httprunner/step_request_test.py b/httprunner/step_request_test.py index f6984b78..7a54e494 100644 --- a/httprunner/step_request_test.py +++ b/httprunner/step_request_test.py @@ -10,7 +10,7 @@ class TestRunRequest(unittest.TestCase): summary = runner.get_summary() self.assertTrue(summary.success) self.assertEqual(summary.name, "request methods testcase with functions") - self.assertEqual(len(summary.step_datas), 3) - self.assertEqual(summary.step_datas[0].name, "get with params") - self.assertEqual(summary.step_datas[1].name, "post raw text") - self.assertEqual(summary.step_datas[2].name, "post form data") + self.assertEqual(len(summary.step_results), 3) + self.assertEqual(summary.step_results[0].name, "get with params") + self.assertEqual(summary.step_results[1].name, "post raw text") + self.assertEqual(summary.step_results[2].name, "post form data") diff --git a/httprunner/step_testcase.py b/httprunner/step_testcase.py index 337ab21f..8ae59eb4 100644 --- a/httprunner/step_testcase.py +++ b/httprunner/step_testcase.py @@ -3,14 +3,14 @@ from typing import Callable, Text from loguru import logger from httprunner import exceptions -from httprunner.models import IStep, StepData, TStep +from httprunner.models import IStep, StepResult, TStep, TestCaseSummary from httprunner.runner import HttpRunner from httprunner.step_request import call_hooks -def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: +def run_step_testcase(runner: HttpRunner, step: TStep) -> StepResult: """run teststep: referenced testcase""" - step_data = StepData(name=step.name) + step_result = StepResult(name=step.name) step_variables = step.variables step_export = step.export @@ -32,15 +32,15 @@ def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData: if step.teardown_hooks: call_hooks(runner, step.teardown_hooks, step.variables, "teardown testcase") - summary = ref_case_runner.get_summary() - step_data.data = summary.step_datas # list of step data - step_data.export_vars = summary.in_out.export_vars - step_data.success = summary.success + summary: TestCaseSummary = ref_case_runner.get_summary() + step_result.data = summary.step_results # list of step data + step_result.export_vars = summary.in_out.export_vars + step_result.success = summary.success - if step_data.export_vars: - logger.info(f"export variables: {step_data.export_vars}") + if step_result.export_vars: + logger.info(f"export variables: {step_result.export_vars}") - return step_data + return step_result class StepRefCase(IStep): diff --git a/httprunner/step_testcase_test.py b/httprunner/step_testcase_test.py index 926e8897..27a7301c 100644 --- a/httprunner/step_testcase_test.py +++ b/httprunner/step_testcase_test.py @@ -12,12 +12,12 @@ class TestRunTestCase(unittest.TestCase): def test_run_testcase_by_path(self): - step_data = RunTestCase("run referenced testcase").call( + step_result = RunTestCase("run referenced testcase").call( TestCaseRequestWithFunctions ).run(self.runner) - self.assertTrue(step_data.success) - self.assertEqual(step_data.name, "run referenced testcase") - self.assertEqual(len(step_data.data), 3) - self.assertEqual(step_data.data[0].name, "get with params") - self.assertEqual(step_data.data[1].name, "post raw text") - self.assertEqual(step_data.data[2].name, "post form data") + self.assertTrue(step_result.success) + self.assertEqual(step_result.name, "run referenced testcase") + self.assertEqual(len(step_result.data), 3) + self.assertEqual(step_result.data[0].name, "get with params") + self.assertEqual(step_result.data[1].name, "post raw text") + self.assertEqual(step_result.data[2].name, "post form data") From 96117bcc1de2dcd3f2ae304b99a6ed00d3b3e855 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 3 Apr 2022 00:06:24 +0800 Subject: [PATCH 06/12] feat: add ConfigThrift for thrift rpc --- httprunner/config.py | 105 +++++++++++++++++++++++++------------------ httprunner/models.py | 11 +++++ 2 files changed, 73 insertions(+), 43 deletions(-) diff --git a/httprunner/config.py b/httprunner/config.py index 2a449ddb..fb5a4b31 100644 --- a/httprunner/config.py +++ b/httprunner/config.py @@ -1,60 +1,79 @@ import inspect from typing import Text -from httprunner.models import TConfig +from httprunner.models import TConfig, TConfigThrift -class Config(object): - def __init__(self, name: Text): - self.__name = name - self.__variables = {} - self.__base_url = "" - self.__verify = False - self.__export = [] - self.__weight = 1 +class ConfigThrift(object): - caller_frame = inspect.stack()[1] - self.__path = caller_frame.filename + def __init__(self, config: TConfig) -> None: + self.__config = config + self.__config.thrift = TConfigThrift() - @property - def name(self) -> Text: - return self.__name - - @property - def path(self) -> Text: - return self.__path - - @property - def weight(self) -> int: - return self.__weight - - def variables(self, **variables) -> "Config": - self.__variables.update(variables) + def psm(self, psm: Text) -> "ConfigThrift": + self.__config.thrift.psm = psm return self - def base_url(self, base_url: Text) -> "Config": - self.__base_url = base_url + def env(self, env: Text) -> "ConfigThrift": + self.__config.thrift.env = env return self - def verify(self, verify: bool) -> "Config": - self.__verify = verify + def cluster(self, cluster: Text) -> "ConfigThrift": + self.__config.thrift.cluster = cluster return self - def export(self, *export_var_name: Text) -> "Config": - self.__export.extend(export_var_name) - return self - - def locust_weight(self, weight: int) -> "Config": - self.__weight = weight + def target(self, target: Text) -> "ConfigThrift": + self.__config.thrift.target = target return self def struct(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, - weight=self.__weight, + return self.__config + + +class Config(object): + + def __init__(self, name: Text) -> None: + caller_frame = inspect.stack()[1] + self.__config = TConfig( + name=name, + path=caller_frame.filename ) + + @property + def name(self) -> Text: + return self.__config.name + + @property + def path(self) -> Text: + return self.__config.path + + @property + def weight(self) -> int: + return self.__config.weight + + def variables(self, **variables) -> "Config": + self.__config.variables.update(variables) + return self + + def base_url(self, base_url: Text) -> "Config": + self.__config.base_url = base_url + return self + + def verify(self, verify: bool) -> "Config": + self.__config.verify = verify + return self + + def export(self, *export_var_name: Text) -> "Config": + self.__config.export.extend(export_var_name) + self.__config.export = list(set(self.__config.export)) + return self + + def locust_weight(self, weight: int) -> "Config": + self.__config.weight = weight + return self + + def struct(self) -> TConfig: + return self.__config + + def thrift(self) -> ConfigThrift: + return ConfigThrift(self.__config) diff --git a/httprunner/models.py b/httprunner/models.py index 9ab903ba..dcb2849b 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -28,6 +28,16 @@ class MethodEnum(Text, Enum): PATCH = "PATCH" +# configs for thrift rpc +class TConfigThrift(BaseModel): + psm: Text = None + env: Text = None + cluster: Text = None + target: Text = None + include_dirs: List[Text] = None + thrift_client: Any = None + + class TConfig(BaseModel): name: Name verify: Verify = False @@ -40,6 +50,7 @@ class TConfig(BaseModel): export: Export = [] path: Text = None weight: int = 1 + thrift: TConfigThrift = None class TRequest(BaseModel): From f8798358b2dc6fb73e4f54f828f197035c9991bb Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 3 Apr 2022 00:13:28 +0800 Subject: [PATCH 07/12] change: remove locust for python version --- .../demo_testsuite_yml/request_with_functions_test.py | 1 - .../request_with_testcase_reference_test.py | 1 - .../request_methods/request_with_functions_test.py | 1 - httprunner/cli.py | 2 +- httprunner/config.py | 8 -------- httprunner/make.py | 7 ------- httprunner/models.py | 2 +- 7 files changed, 2 insertions(+), 20 deletions(-) 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 91682f56..97593703 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 @@ -20,7 +20,6 @@ class TestCaseRequestWithFunctions(HttpRunner): .base_url("https://postman-echo.com") .verify(False) .export(*["foo3"]) - .locust_weight(2) ) 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 ab052c17..54d76e41 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 @@ -29,7 +29,6 @@ class TestCaseRequestWithTestcaseReference(HttpRunner): ) .base_url("https://postman-echo.com") .verify(False) - .locust_weight(3) ) 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 3c23b94c..90f14f36 100644 --- a/examples/postman_echo/request_methods/request_with_functions_test.py +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -20,7 +20,6 @@ class TestCaseRequestWithFunctions(HttpRunner): .base_url("https://postman-echo.com") .verify(False) .export(*["foo3"]) - .locust_weight(2) ) teststeps = [ diff --git a/httprunner/cli.py b/httprunner/cli.py index 38676857..1aa29eac 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -93,7 +93,7 @@ def main(): sys.exit(0) extra_args = [] - if len(sys.argv) >= 2 and sys.argv[1] in ["run", "locusts"]: + if len(sys.argv) >= 2 and sys.argv[1] in ["run"]: args, extra_args = parser.parse_known_args() else: args = parser.parse_args() diff --git a/httprunner/config.py b/httprunner/config.py index fb5a4b31..0f372975 100644 --- a/httprunner/config.py +++ b/httprunner/config.py @@ -47,10 +47,6 @@ class Config(object): def path(self) -> Text: return self.__config.path - @property - def weight(self) -> int: - return self.__config.weight - def variables(self, **variables) -> "Config": self.__config.variables.update(variables) return self @@ -68,10 +64,6 @@ class Config(object): self.__config.export = list(set(self.__config.export)) return self - def locust_weight(self, weight: int) -> "Config": - self.__config.weight = weight - return self - def struct(self) -> TConfig: return self.__config diff --git a/httprunner/make.py b/httprunner/make.py index 1ce384a5..8722b965 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -198,9 +198,6 @@ def make_config_chain_style(config: Dict) -> Text: if "export" in config: config_chain_style += f'.export(*{config["export"]})' - if "weight" in config: - config_chain_style += f'.locust_weight({config["weight"]})' - return config_chain_style @@ -484,10 +481,6 @@ def make_testsuite(testsuite: Dict): ) testcase_dict["config"]["variables"].update(testcase_variables) - # override weight - if "weight" in testcase: - testcase_dict["config"]["weight"] = testcase["weight"] - # make testcase testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir) pytest_files_run_set.add(testcase_pytest_path) diff --git a/httprunner/models.py b/httprunner/models.py index dcb2849b..578fe30b 100644 --- a/httprunner/models.py +++ b/httprunner/models.py @@ -49,7 +49,7 @@ class TConfig(BaseModel): # teardown_hooks: Hooks = [] export: Export = [] path: Text = None - weight: int = 1 + # configs for other protocols thrift: TConfigThrift = None From e7b32b8251f0f29f36bb1f309d324bf49555f73a Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 5 Apr 2022 10:31:50 +0800 Subject: [PATCH 08/12] change: do not send GA events in CI environment --- hrp/internal/sdk/client_test.go | 2 +- hrp/internal/sdk/init.go | 5 +++++ httprunner/utils.py | 13 +++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/hrp/internal/sdk/client_test.go b/hrp/internal/sdk/client_test.go index 905b5d13..8c31cf47 100644 --- a/hrp/internal/sdk/client_test.go +++ b/hrp/internal/sdk/client_test.go @@ -10,7 +10,7 @@ func TestSendEvents(t *testing.T) { Action: "SendEvents", Value: 123, } - err := gaClient.SendEvent(event) + err := SendEvent(event) if err != nil { t.Fatal(err) } diff --git a/hrp/internal/sdk/init.go b/hrp/internal/sdk/init.go index 7d36e710..19be945c 100644 --- a/hrp/internal/sdk/init.go +++ b/hrp/internal/sdk/init.go @@ -2,6 +2,7 @@ package sdk import ( "fmt" + "os" "github.com/denisbrodbeck/machineid" "github.com/getsentry/sentry-go" @@ -46,5 +47,9 @@ func init() { } func SendEvent(e IEvent) error { + if os.Getenv("CI") == "true" { + // do not send GA events in CI environment + return nil + } return gaClient.SendEvent(e) } diff --git a/httprunner/utils.py b/httprunner/utils.py index fa8f77bc..68d72163 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -2,6 +2,7 @@ import collections import copy import itertools import json +import os import os.path import platform import uuid @@ -40,8 +41,13 @@ class GAClient(object): 'cid': uuid.getnode(), # Anonymous Client ID 'ua': f'HttpRunner/{__version__}', } + # do not send GA events in CI environment + self.__is_ci = os.getenv("CI") == "true" def track_event(self, category: Text, action: Text, value: int = 0): + if self.__is_ci: + return + data = { 't': 'event', # Event hit type = event 'ec': category, # Required. Event Category. @@ -51,11 +57,14 @@ class GAClient(object): } data.update(self.common_params) try: - self.http_client.post(self.report_url, data=data) + self.http_client.post(self.report_url, data=data, timeout=5) except Exception: # ProxyError, SSLError, ConnectionError pass def track_user_timing(self, category: Text, variable: Text, duration: int): + if self.__is_ci: + return + data = { 't': 'timing', # Event hit type = timing 'utc': category, # Required. user timing category. e.g. jsonLoader @@ -65,7 +74,7 @@ class GAClient(object): } data.update(self.common_params) try: - self.http_client.post(self.report_url, data=data) + self.http_client.post(self.report_url, data=data, timeout=5) except Exception: # ProxyError, SSLError, ConnectionError pass From c2ba010f286c0a6c0c0a6071b66f11f2fe2cb235 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 5 Apr 2022 10:31:50 +0800 Subject: [PATCH 09/12] feat: disable GA events report by setting environment DISABLE_GA=true --- .github/workflows/hrp-release.yml | 3 +++ .github/workflows/hrp-scaffold.yml | 3 +++ .github/workflows/smoketest.yml | 3 +++ .github/workflows/unittest.yml | 3 +++ docs/CHANGELOG.md | 1 + hrp/internal/sdk/client_test.go | 2 +- hrp/internal/sdk/init.go | 5 +++++ httprunner/utils.py | 13 +++++++++++-- 8 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hrp-release.yml b/.github/workflows/hrp-release.yml index 69d53d1f..4dbb86bc 100644 --- a/.github/workflows/hrp-release.yml +++ b/.github/workflows/hrp-release.yml @@ -4,6 +4,9 @@ on: release: types: [created] +env: + DISABLE_GA: "true" + jobs: releases-matrix: name: Release hrp cli binaries diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index 06b8736a..305696ca 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -6,6 +6,9 @@ on: - master pull_request: +env: + DISABLE_GA: "true" + jobs: scaffold-with-python-plugin: strategy: diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index 5dcb41dd..3192312e 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -6,6 +6,9 @@ on: - master pull_request: +env: + DISABLE_GA: "true" + jobs: smoke-test: diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index a02075bf..c4235d0f 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -6,6 +6,9 @@ on: - master pull_request: +env: + DISABLE_GA: "true" + jobs: py-httprunner: runs-on: ${{ matrix.os }} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d938d777..1caebbe1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ - refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine - refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types +- feat: disable GA events report by setting environment `DISABLE_GA=true` **go version** diff --git a/hrp/internal/sdk/client_test.go b/hrp/internal/sdk/client_test.go index 905b5d13..8c31cf47 100644 --- a/hrp/internal/sdk/client_test.go +++ b/hrp/internal/sdk/client_test.go @@ -10,7 +10,7 @@ func TestSendEvents(t *testing.T) { Action: "SendEvents", Value: 123, } - err := gaClient.SendEvent(event) + err := SendEvent(event) if err != nil { t.Fatal(err) } diff --git a/hrp/internal/sdk/init.go b/hrp/internal/sdk/init.go index 7d36e710..dbf7818a 100644 --- a/hrp/internal/sdk/init.go +++ b/hrp/internal/sdk/init.go @@ -2,6 +2,7 @@ package sdk import ( "fmt" + "os" "github.com/denisbrodbeck/machineid" "github.com/getsentry/sentry-go" @@ -46,5 +47,9 @@ func init() { } func SendEvent(e IEvent) error { + if os.Getenv("DISABLE_GA") == "true" { + // do not send GA events in CI environment + return nil + } return gaClient.SendEvent(e) } diff --git a/httprunner/utils.py b/httprunner/utils.py index fa8f77bc..e2cba145 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -2,6 +2,7 @@ import collections import copy import itertools import json +import os import os.path import platform import uuid @@ -40,8 +41,13 @@ class GAClient(object): 'cid': uuid.getnode(), # Anonymous Client ID 'ua': f'HttpRunner/{__version__}', } + # do not send GA events in CI environment + self.__is_ci = os.getenv("DISABLE_GA") == "true" def track_event(self, category: Text, action: Text, value: int = 0): + if self.__is_ci: + return + data = { 't': 'event', # Event hit type = event 'ec': category, # Required. Event Category. @@ -51,11 +57,14 @@ class GAClient(object): } data.update(self.common_params) try: - self.http_client.post(self.report_url, data=data) + self.http_client.post(self.report_url, data=data, timeout=5) except Exception: # ProxyError, SSLError, ConnectionError pass def track_user_timing(self, category: Text, variable: Text, duration: int): + if self.__is_ci: + return + data = { 't': 'timing', # Event hit type = timing 'utc': category, # Required. user timing category. e.g. jsonLoader @@ -65,7 +74,7 @@ class GAClient(object): } data.update(self.common_params) try: - self.http_client.post(self.report_url, data=data) + self.http_client.post(self.report_url, data=data, timeout=5) except Exception: # ProxyError, SSLError, ConnectionError pass From 37be5c6060ac8fe183507ed70931914c651df605 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 5 Apr 2022 13:13:12 +0800 Subject: [PATCH 10/12] fix #1220: parse step url with base url --- hrp/parser.go | 20 ++++++++++++++++---- hrp/parser_test.go | 35 +++++++++++++++++++++++++++-------- httprunner/parser.py | 25 +++++++++++++++++-------- httprunner/parser_test.py | 27 ++++++++++++++++++++++++++- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/hrp/parser.go b/hrp/parser.go index 3a7ea231..1e5c6b07 100644 --- a/hrp/parser.go +++ b/hrp/parser.go @@ -4,6 +4,7 @@ import ( builtinJSON "encoding/json" "fmt" "net/url" + "path" "reflect" "regexp" "strings" @@ -26,18 +27,29 @@ type Parser struct { } func buildURL(baseURL, stepURL string) string { - uConfig, err := url.Parse(baseURL) + uStep, err := url.Parse(stepURL) if err != nil { - log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse baseURL failed") + log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse url failed") return "" } - uStep, err := uConfig.Parse(stepURL) + // step url is absolute url + if uStep.Host != "" { + return stepURL + } + + // step url is relative, based on base url + uConfig, err := url.Parse(baseURL) if err != nil { - log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse stepURL failed") + log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed") return "" } + // merge url + uStep.Scheme = uConfig.Scheme + uStep.Host = uConfig.Host + uStep.Path = path.Join(uConfig.Path, uStep.Path) + // base url missed return uStep.String() } diff --git a/hrp/parser_test.go b/hrp/parser_test.go index 64ba38d4..41b376b8 100644 --- a/hrp/parser_test.go +++ b/hrp/parser_test.go @@ -11,25 +11,44 @@ import ( func TestBuildURL(t *testing.T) { var url string + url = buildURL("https://postman-echo.com", "/get") - if url != "https://postman-echo.com/get" { - t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url) + if !assert.Equal(t, url, "https://postman-echo.com/get") { + t.Fail() + } + url = buildURL("https://postman-echo.com", "get") + if !assert.Equal(t, url, "https://postman-echo.com/get") { + t.Fail() + } + url = buildURL("https://postman-echo.com/", "/get") + if !assert.Equal(t, url, "https://postman-echo.com/get") { + t.Fail() } url = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2") - if url != "https://postman-echo.com/get?a=1&b=2" { - t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url) + if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") { + t.Fail() + } + url = buildURL("https://postman-echo.com/abc", "get?a=1&b=2") + if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") { + t.Fail() + } + + // omit query string in base url + url = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2") + if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") { + t.Fail() } url = buildURL("", "https://postman-echo.com/get") - if url != "https://postman-echo.com/get" { - t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url) + if !assert.Equal(t, url, "https://postman-echo.com/get") { + t.Fail() } // notice: step request url > config base url url = buildURL("https://postman-echo.com", "https://httpbin.org/get") - if url != "https://httpbin.org/get" { - t.Fatalf("buildURL error, %s != 'https://httpbin.org/get'", url) + if !assert.Equal(t, url, "https://httpbin.org/get") { + t.Fail() } } diff --git a/httprunner/parser.py b/httprunner/parser.py index 9f32d721..192521da 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -3,6 +3,7 @@ import builtins import os import re from typing import Any, Callable, Dict, List, Set, Text +from urllib.parse import urljoin, urlparse from loguru import logger from sentry_sdk import capture_exception @@ -10,8 +11,6 @@ from sentry_sdk import capture_exception from httprunner import exceptions, loader, utils from httprunner.models import FunctionsMapping, VariablesMapping -absolute_http_url_regexp = re.compile(r"^https?://", re.I) - # use $$ to escape $ notation dolloar_regex_compile = re.compile(r"\$\$") # variable notation, e.g. ${var} or $var @@ -37,15 +36,25 @@ def parse_string_value(str_value: Text) -> Any: return str_value -def build_url(base_url, path): +def build_url(base_url, step_url): """ prepend url with base_url unless it's already an absolute URL """ - if absolute_http_url_regexp.match(path): - return path - elif base_url: - return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/")) - else: + o_step_url = urlparse(step_url) + if o_step_url.netloc != "": + # step url is absolute url + return step_url + + # step url is relative, based on base url + o_base_url = urlparse(base_url) + if o_base_url.netloc == "": + # missed base url raise exceptions.ParamsError("base url missed!") + path = o_base_url.path.rstrip("/") + "/" + o_step_url.path.lstrip("/") + o_step_url = o_step_url._replace(scheme=o_base_url.scheme) \ + ._replace(netloc=o_base_url.netloc) \ + ._replace(path=path) + return o_step_url.geturl() + def regex_findall_variables(raw_string: Text) -> List[Text]: """ extract all variable names from content, which is in format $variable diff --git a/httprunner/parser_test.py b/httprunner/parser_test.py index 2a77af59..eaed0f78 100644 --- a/httprunner/parser_test.py +++ b/httprunner/parser_test.py @@ -3,11 +3,36 @@ import time import unittest from httprunner import parser -from httprunner.exceptions import VariableNotFound, FunctionNotFound +from httprunner.exceptions import FunctionNotFound, VariableNotFound from httprunner.loader import load_project_meta class TestParserBasic(unittest.TestCase): + + def test_build_url(self): + url = parser.build_url("https://postman-echo.com", "/get") + self.assertEqual(url, "https://postman-echo.com/get") + url = parser.build_url("https://postman-echo.com", "get") + self.assertEqual(url, "https://postman-echo.com/get") + url = parser.build_url("https://postman-echo.com/", "/get") + self.assertEqual(url, "https://postman-echo.com/get") + + url = parser.build_url("https://postman-echo.com/abc/", "/get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + url = parser.build_url("https://postman-echo.com/abc/", "get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + + # omit query string in base url + url = parser.build_url("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + + url = parser.build_url("", "https://postman-echo.com/get") + self.assertEqual(url, "https://postman-echo.com/get") + + # notice: step request url > config base url + url = parser.build_url("https://postman-echo.com", "https://httpbin.org/get") + self.assertEqual(url, "https://httpbin.org/get") + def test_parse_variables_mapping(self): variables = {"varA": "$varB", "varB": "$varC", "varC": "123", "a": 1, "b": 2} parsed_variables = parser.parse_variables_mapping(variables) From 6d11f1264f130741637a33b9c0f9e2efa076f0d1 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Tue, 5 Apr 2022 13:41:52 +0800 Subject: [PATCH 11/12] fix: panic by data racing --- hrp/step_testcase.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go index ea412058..031133a5 100644 --- a/hrp/step_testcase.go +++ b/hrp/step_testcase.go @@ -44,25 +44,27 @@ func (s *StepTestCaseWithOptionalArgs) Struct() *TStep { } func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) { - stepVariables, err := r.MergeStepVariables(s.step.Variables) - if err != nil { - return nil, err - } - s.step.Variables = stepVariables - stepResult := &StepResult{ Name: s.step.Name, StepType: stepTypeTestCase, Success: false, } - testcase := s.step.TestCase.(*TestCase) - // copy testcase to avoid data racing - copiedTestCase := &TestCase{} - if err := copier.Copy(copiedTestCase, testcase); err != nil { - log.Error().Err(err).Msg("copy testcase failed") + stepVariables, err := r.MergeStepVariables(s.step.Variables) + if err != nil { return stepResult, err } + + // copy step to avoid data racing + copiedStep := &TStep{} + if err := copier.Copy(copiedStep, s.step); err != nil { + log.Error().Err(err).Msg("copy step failed") + return stepResult, err + } + + copiedStep.Variables = stepVariables + copiedTestCase := copiedStep.TestCase.(*TestCase) + // override testcase config extendWithTestCase(s.step, copiedTestCase) From 5e40e3692d1237edc2bf9fa0967767808e820630 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 6 Apr 2022 13:34:50 +0800 Subject: [PATCH 12/12] fix: avoid panic when running tests --- hrp/boomer.go | 7 +++++-- hrp/internal/builtin/utils.go | 4 ++-- hrp/runner_test.go | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/hrp/boomer.go b/hrp/boomer.go index 9a1d20fa..f66679d3 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -1,6 +1,7 @@ package hrp import ( + "os" "sync" "time" @@ -42,14 +43,16 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) { // load all testcases testCases, err := loadTestCases(testcases...) if err != nil { - panic(err) + log.Error().Err(err).Msg("failed to load testcases") + os.Exit(1) } for _, testcase := range testCases { cfg := testcase.Config err = initParameterIterator(cfg, "boomer") if err != nil { - panic(err) + log.Error().Err(err).Msg("failed to init parameter iterator") + os.Exit(1) } rendezvousList := initRendezvous(testcase, int64(b.GetSpawnCount())) task := b.convertBoomerTask(testcase, rendezvousList) diff --git a/hrp/internal/builtin/utils.go b/hrp/internal/builtin/utils.go index 6f8b4442..2e70f906 100644 --- a/hrp/internal/builtin/utils.go +++ b/hrp/internal/builtin/utils.go @@ -227,14 +227,14 @@ func loadFromCSV(path string) []map[string]interface{} { file, err := readFile(path) if err != nil { log.Error().Err(err).Msg("read csv file failed") - panic(err) + os.Exit(1) } r := csv.NewReader(strings.NewReader(string(file))) content, err := r.ReadAll() if err != nil { log.Error().Err(err).Msg("parse csv file failed") - panic(err) + os.Exit(1) } var result []map[string]interface{} for i := 1; i < len(content); i++ { diff --git a/hrp/runner_test.go b/hrp/runner_test.go index df09eb88..5ea57e51 100644 --- a/hrp/runner_test.go +++ b/hrp/runner_test.go @@ -16,7 +16,8 @@ func buildHashicorpGoPlugin() { cmd := exec.Command("go", "build", "-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go") if err := cmd.Run(); err != nil { - panic(err) + log.Error().Err(err).Msg("build hashicorp go plugin failed") + os.Exit(1) } } @@ -30,7 +31,8 @@ func buildHashicorpPyPlugin() { pluginFile := templatesDir + "debugtalk.py" err := scaffold.CopyFile("templates/plugin/debugtalk.py", pluginFile) if err != nil { - panic(err) + log.Error().Err(err).Msg("build hashicorp python plugin failed") + os.Exit(1) } }