Files
httprunner/httprunner/ext/make/__init__.py

306 lines
9.5 KiB
Python

import os
import subprocess
from typing import Text, List, Tuple, Dict, Set, NoReturn
import jinja2
from loguru import logger
from httprunner import exceptions
from httprunner.loader import (
load_folder_files,
load_test_file,
load_testcase,
load_testsuite,
load_project_meta,
)
from httprunner.parser import parse_data
""" cache converted pytest files, avoid duplicate making
"""
make_files_cache_set: Set = set()
__TEMPLATE__ = jinja2.Template(
"""# NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: {{ testcase_path }}
{% if imports_list %}
import os
import sys
sys.path.insert(0, os.getcwd())
{% endif %}
from httprunner import HttpRunner, TConfig, TStep
{% for import_str in imports_list %}
{{ import_str }}
{% endfor %}
class {{ class_name }}(HttpRunner):
config = TConfig(**{{ config }})
teststeps = [
{% for teststep in teststeps %}
TStep(**{{ teststep }}),
{% endfor %}
]
if __name__ == "__main__":
{{ class_name }}().test_start()
"""
)
def __ensure_absolute(path: Text) -> Text:
project_meta = load_project_meta(path)
if os.path.isabs(path):
absolute_path = path
else:
absolute_path = os.path.join(project_meta.PWD, path)
return absolute_path
def __ensure_cwd_relative(path: Text) -> Text:
""" convert absolute path to relative path, based on os.getcwd()
Args:
path: absolute path
Returns: relative path based on os.getcwd()
"""
if os.path.isabs(path):
return path[len(os.getcwd()) + 1 :]
else:
return path
def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
"""convert single YAML/JSON testcase path to python file"""
if os.path.isdir(testcase_path):
# folder does not need to convert
return testcase_path, ""
raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_path))
file_suffix = file_suffix.lower()
if file_suffix not in [".json", ".yml", ".yaml"]:
raise exceptions.ParamsError(
"testcase file should have .yaml/.yml/.json suffix"
)
file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_")
testcase_dir = os.path.dirname(testcase_path)
testcase_python_path = os.path.join(testcase_dir, f"{file_name}_test.py")
# convert title case, e.g. request_with_variables => RequestWithVariables
name_in_title_case = file_name.title().replace("_", "")
testcase_cls_name = f"TestCase{name_in_title_case}"
return testcase_python_path, testcase_cls_name
def __format_pytest_with_black(python_paths: List[Text]) -> NoReturn:
logger.info("format pytest cases with black ...")
try:
subprocess.run(["black", *python_paths])
except subprocess.CalledProcessError as ex:
logger.error(ex)
def __make_testcase(testcase: Dict, dir_path: Text = None) -> NoReturn:
"""convert valid testcase dict to pytest file path"""
try:
# validate testcase format
load_testcase(testcase)
except exceptions.TestCaseFormatError as ex:
logger.error(f"TestCaseFormatError: {ex}")
raise
testcase_path = __ensure_absolute(testcase["config"]["path"])
logger.info(f"start to make testcase: {testcase_path}")
testcase_python_path, testcase_cls_name = convert_testcase_path(testcase_path)
if dir_path:
testcase_python_path = os.path.join(
dir_path, os.path.basename(testcase_python_path)
)
global make_files_cache_set
if testcase_python_path in make_files_cache_set:
return
config = testcase["config"]
config["path"] = __ensure_cwd_relative(testcase_python_path)
# parse config variables
config.setdefault("variables", {})
if isinstance(config["variables"], Text):
# get variables by function, e.g. ${get_variables()}
project_meta = load_project_meta(testcase_path)
config["variables"] = parse_data(
config["variables"], {}, project_meta.functions
)
# prepare reference testcase
imports_list = []
teststeps = testcase["teststeps"]
for teststep in teststeps:
if not teststep.get("testcase"):
continue
# make ref testcase pytest file
ref_testcase_path = __ensure_absolute(teststep["testcase"])
__make(ref_testcase_path)
# prepare ref testcase class name
ref_testcase_python_path, ref_testcase_cls_name = convert_testcase_path(
ref_testcase_path
)
teststep["testcase"] = f"CLS_LB({ref_testcase_cls_name})CLS_RB"
# prepare import ref testcase
ref_testcase_python_path = ref_testcase_python_path[len(os.getcwd()) + 1 :]
ref_module_name, _ = os.path.splitext(ref_testcase_python_path)
ref_module_name = ref_module_name.replace(os.sep, ".")
imports_list.append(f"from {ref_module_name} import {ref_testcase_cls_name}")
data = {
"testcase_path": __ensure_cwd_relative(testcase_path),
"class_name": testcase_cls_name,
"config": config,
"teststeps": teststeps,
"imports_list": imports_list,
}
content = __TEMPLATE__.render(data)
content = content.replace("'CLS_LB(", "").replace(")CLS_RB'", "")
with open(testcase_python_path, "w", encoding="utf-8") as f:
f.write(content)
logger.info(f"generated testcase: {testcase_python_path}")
make_files_cache_set.add(__ensure_cwd_relative(testcase_python_path))
def __make_testsuite(testsuite: Dict) -> NoReturn:
"""convert valid testsuite dict to pytest folder with testcases"""
try:
# validate testcase format
load_testsuite(testsuite)
except exceptions.TestSuiteFormatError as ex:
logger.error(f"TestSuiteFormatError: {ex}")
raise
config = testsuite["config"]
testsuite_path = config["path"]
testsuite_variables = config.get("variables", {})
if isinstance(testsuite_variables, Text):
# get variables by function, e.g. ${get_variables()}
project_meta = load_project_meta(testsuite_path)
testsuite_variables = parse_data(
testsuite_variables, {}, project_meta.functions
)
logger.info(f"start to make testsuite: {testsuite_path}")
# create directory with testsuite file name, put its testcases under this directory
testsuite_dir = os.path.join(
os.path.dirname(testsuite_path),
os.path.basename(testsuite_path).replace(".", "_"),
)
os.makedirs(testsuite_dir, exist_ok=True)
for testcase in testsuite["testcases"]:
# get referenced testcase content
testcase_file = testcase["testcase"]
testcase_path = __ensure_absolute(testcase_file)
testcase_dict = load_test_file(testcase_path)
testcase_dict.setdefault("config", {})
testcase_dict["config"]["path"] = testcase_path
# override testcase name
testcase_dict["config"]["name"] = testcase["name"]
# override base_url
base_url = testsuite["config"].get("base_url") or testcase.get("base_url")
if base_url:
testcase_dict["config"]["base_url"] = base_url
# override variables
testcase_dict["config"].setdefault("variables", {})
testcase_dict["config"]["variables"].update(testcase.get("variables", {}))
testcase_dict["config"]["variables"].update(testsuite_variables)
# make testcase
__make_testcase(testcase_dict, testsuite_dir)
def __make(tests_path: Text) -> NoReturn:
""" make testcase(s) with testcase/testsuite/folder absolute path
generated pytest file path will be cached in make_files_cache_set
Args:
tests_path: should be in absolute path
"""
test_files = []
if os.path.isdir(tests_path):
files_list = load_folder_files(tests_path)
test_files.extend(files_list)
elif os.path.isfile(tests_path):
test_files.append(tests_path)
else:
raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")
for test_file in test_files:
try:
test_content = load_test_file(test_file)
test_content.setdefault("config", {})["path"] = test_file
except (exceptions.FileNotFound, exceptions.FileFormatError) as ex:
logger.warning(ex)
continue
# testcase
if "teststeps" in test_content:
try:
__make_testcase(test_content)
except exceptions.TestCaseFormatError:
continue
# testsuite
elif "testcases" in test_content:
try:
__make_testsuite(test_content)
except exceptions.TestSuiteFormatError:
continue
# invalid format
else:
raise exceptions.FileFormatError(
f"test file is neither testcase nor testsuite: {test_file}"
)
def main_make(tests_paths: List[Text]) -> List[Text]:
for tests_path in tests_paths:
if not os.path.isabs(tests_path):
tests_path = os.path.join(os.getcwd(), tests_path)
__make(tests_path)
testcase_path_list = list(make_files_cache_set)
__format_pytest_with_black(testcase_path_list)
return testcase_path_list
def init_make_parser(subparsers):
""" make testcases: parse command line options and run commands.
"""
parser = subparsers.add_parser(
"make", help="Convert YAML/JSON testcases to pytest cases.",
)
parser.add_argument(
"testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path"
)
return parser