def _extract_field_with_regex(self, field): """ extract field from response content with regex. requests.Response body could be json or html text. Args: field (str): regex string that matched r".*\(.*\).*" Returns: str: matched content. Raises: exceptions.ExtractFailure: If no content matched with regex. Examples: >>> # self.text: "LB123abcRB789" >>> filed = "LB[\d]*(.*)RB[\d]*" >>> _extract_field_with_regex(field) abc """ matched = re.search(field, self.text) if not matched: err_msg = u"Failed to extract data with regex! => {}\n".format(field) err_msg += u"response body: {}\n".format(self.text) logger.log_error(err_msg) raise exceptions.ExtractFailure(err_msg) return matched.group(1)
def query_json(json_content, query, delimiter='.'): """ Do an xpath-like query with json_content. Args: json_content (dict/list/string): content to be queried. query (str): query string. delimiter (str): delimiter symbol. Returns: str: queried result. Examples: >>> json_content = { "ids": [1, 2, 3, 4], "person": { "name": { "first_name": "Leo", "last_name": "Lee", }, "age": 29, "cities": ["Guangzhou", "Shenzhen"] } } >>> >>> query_json(json_content, "person.name.first_name") >>> Leo >>> >>> query_json(json_content, "person.name.first_name.0") >>> L >>> >>> query_json(json_content, "person.cities.0") >>> Guangzhou """ raise_flag = False response_body = u"response body: {}\n".format(json_content) try: for key in query.split(delimiter): if isinstance(json_content, (list, basestring)): json_content = json_content[int(key)] elif isinstance(json_content, dict): json_content = json_content[key] else: logger.log_error("invalid type value: {}({})".format( json_content, type(json_content))) raise_flag = True except (KeyError, ValueError, IndexError): raise_flag = True if raise_flag: err_msg = u"Failed to extract! => {}\n".format(query) err_msg += response_body logger.log_error(err_msg) raise exceptions.ExtractFailure(err_msg) return json_content
def load_json_file(json_file): """ load json file and check file content format """ with io.open(json_file, encoding='utf-8') as data_file: try: json_content = json.load(data_file) except exceptions.JSONDecodeError: err_msg = u"JSONDecodeError: JSON file format error: {}".format( json_file) logger.log_error(err_msg) raise exceptions.FileFormatError(err_msg) _check_format(json_file, json_content) return json_content
def _check_format(file_path, content): """ check testcase format if valid """ # TODO: replace with JSON schema validation if not content: # testcase file content is empty err_msg = u"Testcase file content is empty: {}".format(file_path) logger.log_error(err_msg) raise exceptions.FileFormatError(err_msg) elif not isinstance(content, (list, dict)): # testcase file content does not match testcase format err_msg = u"Testcase file content format invalid: {}".format(file_path) logger.log_error(err_msg) raise exceptions.FileFormatError(err_msg)
def __getattr__(self, key): try: if key == "json": value = self.resp_obj.json() elif key == "cookies": value = self.resp_obj.cookies.get_dict() else: value = getattr(self.resp_obj, key) self.__dict__[key] = value return value except AttributeError: err_msg = "ResponseObject does not have attribute: {}".format(key) logger.log_error(err_msg) raise exceptions.ParamsError(err_msg)
def extract_field(self, field): """ extract value from requests.Response. """ if not isinstance(field, basestring): err_msg = u"Invalid extractor! => {}\n".format(field) logger.log_error(err_msg) raise exceptions.ParamsError(err_msg) msg = "extract: {}".format(field) if text_extractor_regexp_compile.match(field): value = self._extract_field_with_regex(field) else: value = self._extract_field_with_delimiter(field) if is_py2 and isinstance(value, unicode): value = value.encode("utf-8") msg += "\t=> {}".format(value) logger.log_debug(msg) return value
def main_locust(): """ Performance test with locust: parse command line options and run commands. """ # monkey patch ssl at beginning to avoid RecursionError when running locust. from gevent import monkey; monkey.patch_ssl() import multiprocessing import sys from apiautotest import logger try: from apiautotest import locusts except ImportError: msg = "Locust is not installed, install first and try again.\n" msg += "install command: pip install locustio" print(msg) exit(1) sys.argv[0] = 'locust' if len(sys.argv) == 1: sys.argv.extend(["-h"]) if sys.argv[1] in ["-h", "--help", "-V", "--version"]: locusts.start_locust_main() sys.exit(0) # set logging level if "-L" in sys.argv: loglevel_index = sys.argv.index('-L') + 1 elif "--loglevel" in sys.argv: loglevel_index = sys.argv.index('--loglevel') + 1 else: loglevel_index = None if loglevel_index and loglevel_index < len(sys.argv): loglevel = sys.argv[loglevel_index] else: # default loglevel = "WARNING" logger.setup_logger(loglevel) # get testcase file path try: if "-f" in sys.argv: testcase_index = sys.argv.index('-f') + 1 elif "--locustfile" in sys.argv: testcase_index = sys.argv.index('--locustfile') + 1 else: testcase_index = None assert testcase_index and testcase_index < len(sys.argv) except AssertionError: print("Testcase file is not specified, exit.") sys.exit(1) testcase_file_path = sys.argv[testcase_index] sys.argv[testcase_index] = locusts.parse_locustfile(testcase_file_path) if "--processes" in sys.argv: """ locusts -f locustfile.py --processes 4 """ if "--no-web" in sys.argv: logger.log_error("conflict parameter args: --processes & --no-web. \nexit.") sys.exit(1) processes_index = sys.argv.index('--processes') processes_count_index = processes_index + 1 if processes_count_index >= len(sys.argv): """ do not specify processes count explicitly locusts -f locustfile.py --processes """ processes_count = multiprocessing.cpu_count() logger.log_warning("processes count not specified, use {} by default.".format(processes_count)) else: try: """ locusts -f locustfile.py --processes 4 """ processes_count = int(sys.argv[processes_count_index]) sys.argv.pop(processes_count_index) except ValueError: """ locusts -f locustfile.py --processes -P 8888 """ processes_count = multiprocessing.cpu_count() logger.log_warning("processes count not specified, use {} by default.".format(processes_count)) sys.argv.pop(processes_index) locusts.run_locusts_with_processes(sys.argv, processes_count) else: locusts.start_locust_main()
def main_hrun(): """ API test: parse command line options and run commands. """ import argparse from apiautotest import logger from apiautotest.__about__ import __description__, __version__ from apiautotest.api import apiautotest from apiautotest.compat import is_py2 from apiautotest.validator import validate_json_file from apiautotest.utils import (create_scaffold, get_python2_retire_msg, prettify_json_file) parser = argparse.ArgumentParser(description=__description__) parser.add_argument( '-V', '--version', dest='version', action='store_true', help="show version") parser.add_argument( 'testcase_paths', nargs='*', help="testcase file path") parser.add_argument( '--log-level', default='INFO', help="Specify logging level, default is INFO.") parser.add_argument( '--log-file', help="Write logs to specified file path.") parser.add_argument( '--dot-env-path', help="Specify .env file path, which is useful for keeping sensitive data.") parser.add_argument( '--report-template', help="specify report template path.") parser.add_argument( '--report-dir', help="specify report save directory.") parser.add_argument( '--failfast', action='store_true', default=False, help="Stop the test run on the first error or failure.") parser.add_argument( '--save-tests', action='store_true', default=False, help="Save loaded tests and parsed tests to JSON file.") parser.add_argument( '--startproject', help="Specify new project name.") parser.add_argument( '--validate', nargs='*', help="Validate JSON testcase format.") parser.add_argument( '--prettify', nargs='*', help="Prettify JSON testcase format.") args = parser.parse_args() logger.setup_logger(args.log_level, args.log_file) if is_py2: logger.log_warning(get_python2_retire_msg()) if args.version: logger.color_print("{}".format(__version__), "GREEN") exit(0) if args.validate: validate_json_file(args.validate) exit(0) if args.prettify: prettify_json_file(args.prettify) exit(0) project_name = args.startproject if project_name: create_scaffold(project_name) exit(0) runner = apiautotest( failfast=args.failfast, save_tests=args.save_tests, report_template=args.report_template, report_dir=args.report_dir ) try: for path in args.testcase_paths: runner.run(path, dot_env_path=args.dot_env_path) except Exception: logger.log_error("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage)) raise return 0
def _run_test(self, test_dict): """ run single teststep. Args: test_dict (dict): teststep info { "name": "teststep description", "skip": "skip this test unconditionally", "times": 3, "variables": [], # optional, override "request": { "url": "http://127.0.0.1:5000/api/users/1000", "method": "POST", "headers": { "Content-Type": "application/json", "authorization": "$authorization", "random": "$random" }, "json": {"name": "user", "password": "******"} }, "extract": {}, # optional "validate": [], # optional "setup_hooks": [], # optional "teardown_hooks": [] # optional } Raises: exceptions.ParamsError exceptions.ValidationFailure exceptions.ExtractFailure """ global tmp_extracted_variables # clear meta data first to ensure independence for each test self.__clear_test_data() # check skip self._handle_skip_feature(test_dict) # prepare test_dict = utils.lower_test_dict_keys(test_dict) test_variables = test_dict.get("variables", {}) # override variables use former extracted_variables test_variables.update(tmp_extracted_variables) self.session_context.init_test_variables(test_variables) # teststep name test_name = self.session_context.eval_content(test_dict.get( "name", "")) # parse test request raw_request = test_dict.get('request', {}) parsed_test_request = self.session_context.eval_content(raw_request) self.session_context.update_test_variables("request", parsed_test_request) # prepend url with base_url unless it's already an absolute URL url = parsed_test_request.pop('url') base_url = self.session_context.eval_content( test_dict.get("base_url", "")) parsed_url = utils.build_url(base_url, url) # setup hooks setup_hooks = test_dict.get("setup_hooks", []) if setup_hooks: self.do_hook_actions(setup_hooks, "setup") try: method = parsed_test_request.pop('method') parsed_test_request.setdefault("verify", self.verify) group_name = parsed_test_request.pop("group", None) except KeyError: raise exceptions.ParamsError("URL or METHOD missed!") # TODO: move method validation to json schema valid_methods = [ "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" ] if method.upper() not in valid_methods: err_msg = u"Invalid HTTP method! => {}\n".format(method) err_msg += "Available HTTP methods: {}".format( "/".join(valid_methods)) logger.log_error(err_msg) raise exceptions.ParamsError(err_msg) logger.log_info("{method} {url}".format(method=method, url=parsed_url)) logger.log_debug( "request kwargs(raw): {kwargs}".format(kwargs=parsed_test_request)) # request resp = self.http_client_session.request(method, parsed_url, name=(group_name or test_name), **parsed_test_request) resp_obj = response.ResponseObject(resp) # extract extractors = test_dict.get("extract", {}) if extractors: extracted_variables_mapping = resp_obj.extract_response(extractors) tmp_extracted_variables.update(extracted_variables_mapping) self.session_context.update_session_variables( extracted_variables_mapping) self.session_context.update_session_variables(tmp_extracted_variables) # validate validators = test_dict.get("validate") or test_dict.get( "validators") or [] try: self.session_context.validate(validators, resp_obj) except (exceptions.ParamsError, exceptions.ValidationFailure, exceptions.ExtractFailure): err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format( "*" * 32, "*" * 32) # log request err_msg += "====== request details ======\n" err_msg += "url: {}\n".format(parsed_url) err_msg += "method: {}\n".format(method) err_msg += "headers: {}\n".format( parsed_test_request.pop("headers", {})) for k, v in parsed_test_request.items(): v = utils.omit_long_data(v) err_msg += "{}: {}\n".format(k, repr(v)) err_msg += "\n" # log response err_msg += "====== response details ======\n" err_msg += "status_code: {}\n".format(resp_obj.status_code) err_msg += "headers: {}\n".format(resp_obj.headers) err_msg += "body: {}\n".format(repr(resp_obj.text)) logger.log_error(err_msg) raise finally: self.validation_results = self.session_context.validation_results # teardown hooks teardown_hooks = test_dict.get("teardown_hooks", []) if teardown_hooks: self.session_context.update_test_variables( "response", resp_obj) self.do_hook_actions(teardown_hooks, "teardown")
def request(self, method, url, name=None, **kwargs): """ Constructs and sends a :py:class:`requests.Request`. Returns :py:class:`requests.Response` object. :param method: method for the new :class:`Request` object. :param url: URL for the new :class:`Request` object. :param name: (optional) Placeholder, make compatible with Locust's HttpSession :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. :param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload. :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. :param timeout: (optional) How long to wait for the server to send data before giving up, as a float, or \ a (`connect timeout, read timeout <user/advanced.html#timeouts>`_) tuple. :type timeout: float or tuple :param allow_redirects: (optional) Set to True by default. :type allow_redirects: bool :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. :param stream: (optional) whether to immediately download the response content. Defaults to ``False``. :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. """ self.init_meta_data() # record test name self.meta_data["name"] = name # record original request info self.meta_data["data"][0]["request"]["method"] = method self.meta_data["data"][0]["request"]["url"] = url kwargs.setdefault("timeout", 120) self.meta_data["data"][0]["request"].update(kwargs) start_timestamp = time.time() response = self._send_request_safe_mode(method, url, **kwargs) response_time_ms = round((time.time() - start_timestamp) * 1000, 2) # get the length of the content, but if the argument stream is set to True, we take # the size from the content-length header, in order to not trigger fetching of the body if kwargs.get("stream", False): content_size = int( dict(response.headers).get("content-length") or 0) else: content_size = len(response.content or "") # record the consumed time self.meta_data["stat"] = { "response_time_ms": response_time_ms, "elapsed_ms": response.elapsed.microseconds / 1000.0, "content_size": content_size } # record request and response histories, include 30X redirection response_list = response.history + [response] self.meta_data["data"] = [ self.get_req_resp_record(resp_obj) for resp_obj in response_list ] try: response.raise_for_status() except RequestException as e: logger.log_error(u"{exception}".format(exception=str(e))) else: logger.log_info( """status_code: {}, response_time(ms): {} ms, response_length: {} bytes\n""" .format(response.status_code, response_time_ms, content_size)) return response
def load_tests(path, dot_env_path=None): """ load testcases from file path, extend and merge with api/testcase definitions. Args: path (str): testcase/testsuite file/foler path. path could be in 2 types: - absolute/relative file path - absolute/relative folder path dot_env_path (str): specified .env file path Returns: dict: tests mapping, include project_mapping and testcases. each testcase is corresponding to a file. { "project_mapping": { "PWD": "XXXXX", "functions": {}, "env": {} }, "testcases": [ { # testcase data structure "config": { "name": "desc1", "path": "testcase1_path", "variables": [], # optional }, "teststeps": [ # test data structure { 'name': 'test desc1', 'variables': [], # optional 'extract': [], # optional 'validate': [], 'request': {} }, test_dict_2 # another test dict ] }, testcase_2_dict # another testcase dict ], "testsuites": [ { # testsuite data structure "config": {}, "testcases": { "testcase1": {}, "testcase2": {}, } }, testsuite_2_dict ] } """ if not os.path.exists(path): err_msg = "path not exist: {}".format(path) logger.log_error(err_msg) raise exceptions.FileNotFound(err_msg) if not os.path.isabs(path): path = os.path.join(os.getcwd(), path) load_project_tests(path, dot_env_path) tests_mapping = {"project_mapping": project_mapping} def __load_file_content(path): loaded_content = load_test_file(path) if not loaded_content: pass elif loaded_content["type"] == "testsuite": tests_mapping.setdefault("testsuites", []).append(loaded_content) elif loaded_content["type"] == "testcase": tests_mapping.setdefault("testcases", []).append(loaded_content) elif loaded_content["type"] == "api": tests_mapping.setdefault("apis", []).append(loaded_content) if os.path.isdir(path): files_list = load_folder_files(path) for path in files_list: __load_file_content(path) elif os.path.isfile(path): __load_file_content(path) return tests_mapping
def _extract_field_with_delimiter(self, field): """ response content could be json or html text. Args: field (str): string joined by delimiter. e.g. "status_code" "headers" "cookies" "content" "headers.content-type" "content.person.name.first_name" """ # string.split(sep=None, maxsplit=-1) -> list of strings # e.g. "content.person.name" => ["content", "person.name"] try: top_query, sub_query = field.split('.', 1) except ValueError: top_query = field sub_query = None # status_code if top_query in ["status_code", "encoding", "ok", "reason", "url"]: if sub_query: # status_code.XX err_msg = u"Failed to extract: {}\n".format(field) logger.log_error(err_msg) raise exceptions.ParamsError(err_msg) return getattr(self, top_query) # cookies elif top_query == "cookies": cookies = self.cookies if not sub_query: # extract cookies return cookies try: return cookies[sub_query] except KeyError: err_msg = u"Failed to extract cookie! => {}\n".format(field) err_msg += u"response cookies: {}\n".format(cookies) logger.log_error(err_msg) raise exceptions.ExtractFailure(err_msg) # elapsed elif top_query == "elapsed": available_attributes = u"available attributes: days, seconds, microseconds, total_seconds" if not sub_query: err_msg = u"elapsed is datetime.timedelta instance, attribute should also be specified!\n" err_msg += available_attributes logger.log_error(err_msg) raise exceptions.ParamsError(err_msg) elif sub_query in ["days", "seconds", "microseconds"]: return getattr(self.elapsed, sub_query) elif sub_query == "total_seconds": return self.elapsed.total_seconds() else: err_msg = "{} is not valid datetime.timedelta attribute.\n".format(sub_query) err_msg += available_attributes logger.log_error(err_msg) raise exceptions.ParamsError(err_msg) # headers elif top_query == "headers": headers = self.headers if not sub_query: # extract headers return headers try: return headers[sub_query] except KeyError: err_msg = u"Failed to extract header! => {}\n".format(field) err_msg += u"response headers: {}\n".format(headers) logger.log_error(err_msg) raise exceptions.ExtractFailure(err_msg) # response body elif top_query in ["content", "text", "json"]: try: body = self.json except exceptions.JSONDecodeError: body = self.text if not sub_query: # extract response body return body if isinstance(body, (dict, list)): # content = {"xxx": 123}, content.xxx return utils.query_json(body, sub_query) elif sub_query.isdigit(): # content = "abcdefg", content.3 => d return utils.query_json(body, sub_query) else: # content = "<html>abcdefg</html>", content.xxx err_msg = u"Failed to extract attribute from response body! => {}\n".format(field) err_msg += u"response body: {}\n".format(body) logger.log_error(err_msg) raise exceptions.ExtractFailure(err_msg) # new set response attributes in teardown_hooks elif top_query in self.__dict__: attributes = self.__dict__[top_query] if not sub_query: # extract response attributes return attributes if isinstance(attributes, (dict, list)): # attributes = {"xxx": 123}, content.xxx return utils.query_json(attributes, sub_query) elif sub_query.isdigit(): # attributes = "abcdefg", attributes.3 => d return utils.query_json(attributes, sub_query) else: # content = "attributes.new_attribute_not_exist" err_msg = u"Failed to extract cumstom set attribute from teardown hooks! => {}\n".format(field) err_msg += u"response set attributes: {}\n".format(attributes) logger.log_error(err_msg) raise exceptions.TeardownHooksFailure(err_msg) # others else: err_msg = u"Failed to extract attribute from response! => {}\n".format(field) err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n" err_msg += u"If you want to set attribute in teardown_hooks, take the following example as reference:\n" err_msg += u"response.new_attribute = 'new_attribute_value'\n" logger.log_error(err_msg) raise exceptions.ParamsError(err_msg)
def validate(self, validators, resp_obj): """ make validation with comparators """ self.validation_results = [] if not validators: return logger.log_debug("start to validate.") validate_pass = True failures = [] for validator in validators: # validator should be LazyFunction object if not isinstance(validator, parser.LazyFunction): raise exceptions.ValidationFailure( "validator should be parsed first: {}".format(validators)) # evaluate validator args with context variable mapping. validator_args = validator.get_args() check_item, expect_item = validator_args check_value = self.__eval_validator_check(check_item, resp_obj) expect_value = self.__eval_validator_expect(expect_item) validator.update_args([check_value, expect_value]) comparator = validator.func_name validator_dict = { "comparator": comparator, "check": check_item, "check_value": check_value, "expect": expect_item, "expect_value": expect_value } validate_msg = "\nvalidate: {} {} {}({})".format( check_item, comparator, expect_value, type(expect_value).__name__) try: validator.to_value(self.test_variables_mapping) validator_dict["check_result"] = "pass" validate_msg += "\t==> pass" logger.log_debug(validate_msg) except (AssertionError, TypeError): validate_pass = False validator_dict["check_result"] = "fail" validate_msg += "\t==> fail" validate_msg += "\n{}({}) {} {}({})".format( check_value, type(check_value).__name__, comparator, expect_value, type(expect_value).__name__) logger.log_error(validate_msg) failures.append(validate_msg) self.validation_results.append(validator_dict) # restore validator args, in case of running multiple times validator.update_args(validator_args) if not validate_pass: failures_string = "\n".join([failure for failure in failures]) raise exceptions.ValidationFailure(failures_string)