diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 272d8a77..f82a34e1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,10 @@ - feat: integrate [locust](https://locust.io/) v1.0 +**Changed** + +- change: make converted referenced pytest files always relative to ProjectRootDir + **Fixed** - change: do not raise error if failed to get client/server address info 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 ee4ddeb7..63a5b73e 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 @@ -1,14 +1,14 @@ # NOTE: Generated By HttpRunner v3.1.0 # FROM: request_methods/request_with_testcase_reference.yml -import os import sys +from pathlib import Path -sys.path.insert(0, os.getcwd()) +sys.path.insert(0, str(Path(__file__).parent.parent)) from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase -from examples.postman_echo.request_methods.request_with_functions_test import ( +from request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, ) diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py index 5fe21f9a..7154992b 100644 --- a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -1,14 +1,14 @@ # NOTE: Generated By HttpRunner v3.1.0 # FROM: request_methods/request_with_testcase_reference.yml -import os import sys +from pathlib import Path -sys.path.insert(0, os.getcwd()) +sys.path.insert(0, str(Path(__file__).parent.parent)) from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase -from examples.postman_echo.request_methods.request_with_functions_test import ( +from request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, ) diff --git a/httprunner/compat.py b/httprunner/compat.py index 966d4e89..17766ed9 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -6,7 +6,7 @@ import sys from typing import List, Dict, Text, Union, Any from httprunner import exceptions -from httprunner.loader import load_project_meta +from httprunner.loader import load_project_meta, convert_relative_project_root_dir from httprunner.parser import parse_data from httprunner.utils import sort_dict_by_custom_order from loguru import logger @@ -340,7 +340,7 @@ def session_fixture(request): test_path = os.path.abspath(test_path) logs_dir_path = os.path.join(project_root_dir, "logs") - test_path_relative_path = test_path[len(project_root_dir) + 1 :] + test_path_relative_path = convert_relative_project_root_dir(test_path) if os.path.isdir(test_path): file_foder_path = os.path.join(logs_dir_path, test_path_relative_path) diff --git a/httprunner/loader.py b/httprunner/loader.py index c1cbe835..1a9e6484 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -432,3 +432,23 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta: project_meta.debugtalk_path = debugtalk_path return project_meta + + +def convert_relative_project_root_dir(abs_path: Text) -> Text: + """ convert absolute path to relative path, based on project_meta.RootDir + + Args: + abs_path: absolute path + + Returns: relative path based on project_meta.RootDir + + """ + _project_meta = load_project_meta(abs_path) + if not abs_path.startswith(_project_meta.RootDir): + raise exceptions.ParamsError( + f"failed to convert absolute path to relative path based on project_meta.RootDir\n" + f"abs_path: {abs_path}\n" + f"project_meta.RootDir: {_project_meta.RootDir}" + ) + + return abs_path[len(_project_meta.RootDir) + 1 :] diff --git a/httprunner/make.py b/httprunner/make.py index 99bf025b..b7a908d2 100644 --- a/httprunner/make.py +++ b/httprunner/make.py @@ -1,7 +1,7 @@ import os +import string import subprocess import sys -from shutil import copyfile from typing import Text, List, Tuple, Dict, Set, NoReturn import jinja2 @@ -21,9 +21,10 @@ from httprunner.loader import ( load_testcase, load_testsuite, load_project_meta, + convert_relative_project_root_dir, ) from httprunner.response import uniform_validator -from httprunner.utils import ensure_file_abs_path_valid, override_config_variables +from httprunner.utils import override_config_variables """ cache converted pytest files, avoid duplicate making """ @@ -36,11 +37,15 @@ pytest_files_run_set: Set = set() __TEMPLATE__ = jinja2.Template( """# NOTE: Generated By HttpRunner v{{ version }} # FROM: {{ testcase_path }} -{% if imports_list %} -import os +{% if imports_list and diff_levels > 0 %} import sys +from pathlib import Path -sys.path.insert(0, os.getcwd()) +sys.path.insert(0, str(Path(__file__) +{% for _ in range(diff_levels) %} +.parent +{% endfor %} +)) {% endif %} from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase {% for import_str in imports_list %} @@ -86,44 +91,45 @@ def __ensure_absolute(path: Text) -> Text: return absolute_path -def __convert_relative_current_working_dir(abs_path: Text) -> Text: - """ convert absolute path to relative path, based on os.getcwd() +def ensure_file_abs_path_valid(file_abs_path: Text) -> Text: + """ ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space Args: - abs_path: absolute path + file_abs_path: absolute file path - Returns: relative path based on os.getcwd() + Returns: + ensured valid absolute file path """ - cwd = os.getcwd() - if not abs_path.startswith(cwd): - raise exceptions.ParamsError( - f"failed to convert absolute path to relative path based on os.getcwd()\n" - f"abs_path: {abs_path}\n" - f"os.getcwd(): {cwd}" - ) + project_meta = load_project_meta(file_abs_path) + raw_abs_file_name, file_suffix = os.path.splitext(file_abs_path) + file_suffix = file_suffix.lower() - return abs_path[len(cwd) + 1 :] + raw_file_relative_name = convert_relative_project_root_dir(raw_abs_file_name) + if raw_file_relative_name == "": + return file_abs_path + path_names = [] + for name in raw_file_relative_name.rstrip(os.sep).split(os.sep): -def __convert_relative_project_root_dir(abs_path: Text) -> Text: - """ convert absolute path to relative path, based on project_meta.RootDir + if name[0] in string.digits: + # ensure file name not startswith digit + # 19 => T19, 2C => T2C + name = f"T{name}" - Args: - abs_path: absolute path + if name.startswith("."): + # avoid ".csv" been converted to "_csv" + pass + else: + # handle cases when directory name includes dot/hyphen/space + name = name.replace(" ", "_").replace(".", "_").replace("-", "_") - Returns: relative path based on project_meta.RootDir + path_names.append(name) - """ - project_meta = load_project_meta(abs_path) - if not abs_path.startswith(project_meta.RootDir): - raise exceptions.ParamsError( - f"failed to convert absolute path to relative path based on project_meta.RootDir\n" - f"abs_path: {abs_path}\n" - f"project_meta.RootDir: {project_meta.RootDir}" - ) - - return abs_path[len(project_meta.RootDir) + 1 :] + new_file_path = os.path.join( + project_meta.RootDir, f"{os.sep.join(path_names)}{file_suffix}" + ) + return new_file_path def __ensure_testcase_module(path: Text) -> NoReturn: @@ -137,31 +143,6 @@ def __ensure_testcase_module(path: Text) -> NoReturn: f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n") -def __ensure_project_meta_files(tests_path: Text) -> NoReturn: - """ ensure project meta files exist in generated pytest folder files - include debugtalk.py and .env - """ - project_meta = load_project_meta(tests_path) - - # handle cases when generated pytest directory are different from original yaml/json testcases - debugtalk_path = project_meta.debugtalk_path - if debugtalk_path: - debugtalk_new_path = ensure_file_abs_path_valid(debugtalk_path) - if debugtalk_new_path != debugtalk_path: - logger.info(f"copy debugtalk.py to {debugtalk_new_path}") - copyfile(debugtalk_path, debugtalk_new_path) - - global pytest_files_made_cache_mapping - pytest_files_made_cache_mapping[debugtalk_new_path] = "" - - dot_csv_path = project_meta.dot_env_path - if dot_csv_path: - dot_csv_new_path = ensure_file_abs_path_valid(dot_csv_path) - if dot_csv_new_path != dot_csv_path: - logger.info(f"copy .env to {dot_csv_new_path}") - copyfile(dot_csv_path, dot_csv_new_path) - - def convert_testcase_path(testcase_abs_path: Text) -> Tuple[Text, Text]: """convert single YAML/JSON testcase path to python file""" testcase_new_path = ensure_file_abs_path_valid(testcase_abs_path) @@ -358,7 +339,7 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: return testcase_python_abs_path config = testcase["config"] - config["path"] = __convert_relative_project_root_dir(testcase_python_abs_path) + config["path"] = convert_relative_project_root_dir(testcase_python_abs_path) config["variables"] = convert_variables( config.get("variables", {}), testcase_abs_path ) @@ -388,7 +369,7 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: teststep["testcase"] = ref_testcase_cls_name # prepare import ref testcase - ref_testcase_python_relative_path = __convert_relative_current_working_dir( + ref_testcase_python_relative_path = convert_relative_project_root_dir( ref_testcase_python_abs_path ) ref_module_name, _ = os.path.splitext(ref_testcase_python_relative_path) @@ -397,9 +378,14 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}" ) + testcase_path = convert_relative_project_root_dir(testcase_abs_path) + # current file compared to ProjectRootDir + diff_levels = len(testcase_path.split(os.sep)) + data = { "version": __version__, - "testcase_path": __convert_relative_project_root_dir(testcase_abs_path), + "testcase_path": testcase_path, + "diff_levels": diff_levels, "class_name": f"TestCase{testcase_cls_name}", "imports_list": imports_list, "config_chain_style": make_config_chain_style(config), @@ -564,8 +550,6 @@ def main_make(tests_paths: List[Text]) -> List[Text]: logger.error(ex) sys.exit(1) - __ensure_project_meta_files(tests_path) - # format pytest files pytest_files_format_list = pytest_files_made_cache_mapping.keys() format_pytest_with_black(*pytest_files_format_list) diff --git a/httprunner/utils.py b/httprunner/utils.py index 0b22e049..d832f881 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -3,16 +3,14 @@ import copy import json import os.path import platform -import string import uuid -from typing import Dict, List, Any, Text +from typing import Dict, List, Any import sentry_sdk -from loguru import logger - from httprunner import __version__ from httprunner import exceptions from httprunner.models import VariablesMapping +from loguru import logger def init_sentry_sdk(): @@ -181,45 +179,6 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List): ) -def ensure_file_abs_path_valid(file_abs_path: Text) -> Text: - """ ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space - - Args: - file_abs_path: absolute file path - - Returns: - ensured valid absolute file path - - """ - raw_abs_file_name, file_suffix = os.path.splitext(file_abs_path) - file_suffix = file_suffix.lower() - - raw_file_relative_name = raw_abs_file_name[len(os.getcwd()) + 1 :] - - if raw_file_relative_name == "": - return file_abs_path - - path_names = [] - for name in raw_file_relative_name.rstrip(os.sep).split(os.sep): - - if name[0] in string.digits: - # ensure file name not startswith digit - # 19 => T19, 2C => T2C - name = f"T{name}" - - if name.startswith("."): - # avoid ".csv" been converted to "_csv" - pass - else: - # handle cases when directory name includes dot/hyphen/space - name = name.replace(" ", "_").replace(".", "_").replace("-", "_") - - path_names.append(name) - - new_file_path = os.path.join(os.getcwd(), f"{os.sep.join(path_names)}{file_suffix}") - return new_file_path - - class ExtendJSONEncoder(json.JSONEncoder): """ especially used to safely dump json data with python object, such as MultipartEncoder """ diff --git a/tests/cli_test.py b/tests/cli_test.py index c84e294b..863ef0bd 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,4 +1,5 @@ import io +import os import sys import unittest @@ -40,10 +41,12 @@ class TestCli(unittest.TestCase): self.assertIn(__description__, self.captured_output.getvalue().strip()) def test_debug_pytest(self): - exit_code = pytest.main( - [ - "-s", - "examples/postman_echo/request_methods/request_with_testcase_reference_test.py", - ] - ) - self.assertEqual(exit_code, 0) + cwd = os.getcwd() + try: + os.chdir(os.path.join(cwd, "examples", "postman_echo")) + exit_code = pytest.main( + ["-s", "request_methods/request_with_testcase_reference_test.py",] + ) + self.assertEqual(exit_code, 0) + finally: + os.chdir(cwd) diff --git a/tests/data/a-b.c/__init__.py b/tests/data/.csv similarity index 100% rename from tests/data/a-b.c/__init__.py rename to tests/data/.csv diff --git a/tests/data/a-b.c/2 3.yml b/tests/data/a-b.c/2 3.yml index f20d5b4d..8a37b3a8 100644 --- a/tests/data/a-b.c/2 3.yml +++ b/tests/data/a-b.c/2 3.yml @@ -6,7 +6,7 @@ config: teststeps: - name: request with functions - testcase: 1.yml + testcase: a-b.c/1.yml export: - session_foo2 - diff --git a/tests/data/a-b.c/中文case.yml b/tests/data/a-b.c/中文case.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/a-b.c/debugtalk.py b/tests/data/debugtalk.py similarity index 100% rename from tests/data/a-b.c/debugtalk.py rename to tests/data/debugtalk.py diff --git a/tests/make_test.py b/tests/make_test.py index b7f1ec68..f5aeb6ae 100644 --- a/tests/make_test.py +++ b/tests/make_test.py @@ -8,6 +8,7 @@ from httprunner.make import ( make_config_chain_style, make_teststep_chain_style, pytest_files_run_set, + ensure_file_abs_path_valid, ) from httprunner import loader @@ -64,7 +65,7 @@ class TestMake(unittest.TestCase): content = f.read() self.assertIn( """ -from examples.postman_echo.request_methods.request_with_functions_test import ( +from request_methods.request_with_functions_test import ( TestCaseRequestWithFunctions as RequestWithFunctions, ) """, @@ -90,53 +91,57 @@ from examples.postman_echo.request_methods.request_with_functions_test import ( testcase_python_list, ) + def test_ensure_file_path_valid(self): + self.assertEqual( + ensure_file_abs_path_valid( + os.path.join(os.getcwd(), "tests", "data", "a-b.c", "2 3.yml") + ), + os.path.join(os.getcwd(), "tests", "data", "a_b_c", "T2_3.yml"), + ) + loader.project_meta = None + self.assertEqual( + ensure_file_abs_path_valid( + os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods") + ), + os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods"), + ) + loader.project_meta = None + self.assertEqual( + ensure_file_abs_path_valid(os.path.join(os.getcwd(), "README.md")), + os.path.join(os.getcwd(), "README.md"), + ) + loader.project_meta = None + self.assertEqual( + ensure_file_abs_path_valid(os.getcwd()), os.getcwd(), + ) + loader.project_meta = None + self.assertEqual( + ensure_file_abs_path_valid( + os.path.join(os.getcwd(), "tests", "data", ".csv") + ), + os.path.join(os.getcwd(), "tests", "data", ".csv"), + ) + def test_convert_testcase_path(self): - self.assertEqual( - convert_testcase_path(os.path.join(os.getcwd(), "mubu.login.yml")), - (os.path.join(os.getcwd(), "mubu_login_test.py"), "MubuLogin"), - ) self.assertEqual( convert_testcase_path( - os.path.join(os.getcwd(), os.path.join("path", "to", "mubu.login.yml")) + os.path.join(os.getcwd(), "tests", "data", "a-b.c", "2 3.yml") ), ( - os.path.join( - os.getcwd(), os.path.join("path", "to", "mubu_login_test.py") - ), - "MubuLogin", + os.path.join(os.getcwd(), "tests", "data", "a_b_c", "T2_3_test.py"), + "T23", ), ) self.assertEqual( convert_testcase_path( - os.path.join(os.getcwd(), "path", "to 2", "mubu.login.yml") + os.path.join(os.getcwd(), "tests", "data", "a-b.c", "中文case.yml") ), ( os.path.join( - os.getcwd(), os.path.join("path", "to_2", "mubu_login_test.py") + os.getcwd(), + os.path.join("tests", "data", "a_b_c", "中文case_test.py"), ), - "MubuLogin", - ), - ) - self.assertEqual( - convert_testcase_path( - os.path.join(os.getcwd(), "path", "to-2", "mubu login.yml") - ), - ( - os.path.join( - os.getcwd(), os.path.join("path", "to_2", "mubu_login_test.py") - ), - "MubuLogin", - ), - ) - self.assertEqual( - convert_testcase_path( - os.path.join(os.getcwd(), "path", "to.2", "幕布login.yml") - ), - ( - os.path.join( - os.getcwd(), os.path.join("path", "to_2", "幕布login_test.py") - ), - "幕布Login", + "中文Case", ), ) diff --git a/tests/runner_test.py b/tests/runner_test.py index b91fd3df..96654d3b 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -35,6 +35,6 @@ class TestHttpRunner(unittest.TestCase): exit_code = main_run(["tests/data/a-b.c/2 3.yml"]) self.assertEqual(exit_code, 0) self.assertTrue(os.path.exists("tests/data/a_b_c/__init__.py")) - self.assertTrue(os.path.exists("tests/data/a_b_c/debugtalk.py")) + self.assertTrue(os.path.exists("tests/data/debugtalk.py")) self.assertTrue(os.path.exists("tests/data/a_b_c/T1_test.py")) self.assertTrue(os.path.exists("tests/data/a_b_c/T2_3_test.py")) diff --git a/tests/utils_test.py b/tests/utils_test.py index bd4e3f53..159ea4e1 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -5,7 +5,6 @@ import unittest from httprunner import loader, utils from httprunner.utils import ( - ensure_file_abs_path_valid, ExtendJSONEncoder, override_config_variables, ) @@ -105,41 +104,6 @@ class TestUtils(unittest.TestCase): ["A", "D", "C", "B"], ) - def test_ensure_file_path_valid(self): - self.assertEqual( - ensure_file_abs_path_valid( - os.path.join(os.getcwd(), "examples", "a-b.c", "d f", "hardcode.yml") - ), - os.path.join(os.getcwd(), "examples", "a_b_c", "d_f", "hardcode.yml"), - ) - self.assertEqual( - ensure_file_abs_path_valid(os.path.join(os.getcwd(), "1", "2B", "3.yml")), - os.path.join(os.getcwd(), "T1", "T2B", "T3.yml"), - ) - self.assertEqual( - ensure_file_abs_path_valid( - os.path.join(os.getcwd(), "examples", "a-b.c", "2B", "hardcode.yml") - ), - os.path.join(os.getcwd(), "examples", "a_b_c", "T2B", "hardcode.yml"), - ) - self.assertEqual( - ensure_file_abs_path_valid( - os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods") - ), - os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods"), - ) - self.assertEqual( - ensure_file_abs_path_valid(os.path.join(os.getcwd(), "test.yml")), - os.path.join(os.getcwd(), "test.yml"), - ) - self.assertEqual( - ensure_file_abs_path_valid(os.getcwd()), os.getcwd(), - ) - self.assertEqual( - ensure_file_abs_path_valid(os.path.join(os.getcwd(), "demo", ".csv")), - os.path.join(os.getcwd(), "demo", ".csv"), - ) - def test_safe_dump_json(self): class A(object): pass