Skip to content

Commit 2bed4ec

Browse files
committed
支持生成conftest
1 parent c86fa12 commit 2bed4ec

7 files changed

Lines changed: 203 additions & 25 deletions

File tree

‎pytest/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "testsolar-pytestx"
3-
version = "0.1.66"
3+
version = "0.1.67"
44
description = "Pytest tool for TestSolar"
55
authors = [
66
{name = "asiazhang2002", email = "asiazhang2002@gmail.com"},
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""
2+
conftest.py 生成器模块
3+
4+
用于在测试目录中生成 conftest.py 文件,确保 xdist worker 进程也能正确初始化 header injection。
5+
6+
当使用 pytest.main(plugins=[PytestExecutor]) 方式运行测试时,
7+
xdist worker 进程不会继承主进程的 plugins 参数,
8+
因此需要通过 conftest.py 来确保每个 worker 进程都能执行初始化。
9+
"""
10+
11+
from pathlib import Path
12+
from typing import Optional
13+
14+
from loguru import logger
15+
16+
# conftest.py 的内容模板
17+
# 注意:这个模板会被写入到测试项目目录中
18+
CONFTEST_TEMPLATE = '''"""
19+
Auto-generated conftest.py for testsolar header injection support.
20+
This file ensures that HTTP request header injection works correctly
21+
even when using pytest-xdist for parallel test execution.
22+
23+
DO NOT MODIFY THIS FILE - it will be regenerated on each test run.
24+
"""
25+
import os
26+
27+
# 仅在启用 API 收集时才初始化
28+
if os.environ.get("ENABLE_API_COLLECTING") == "1":
29+
try:
30+
from testsolar_pytestx.header_injection import (
31+
initialize_header_injection,
32+
set_current_test_nodeid,
33+
)
34+
35+
# 在模块导入时就初始化,确保在 httpx 被导入之前完成 patch
36+
initialize_header_injection()
37+
38+
except ImportError:
39+
# testsolar_pytestx 未安装,跳过
40+
pass
41+
'''
42+
43+
# 用于标识自动生成的 conftest.py 的标记
44+
CONFTEST_MARKER = "Auto-generated conftest.py for testsolar header injection support."
45+
46+
47+
def _is_testsolar_conftest(conftest_path: Path) -> bool:
48+
"""检查 conftest.py 是否是由 testsolar 生成的"""
49+
if not conftest_path.exists():
50+
return False
51+
52+
try:
53+
content = conftest_path.read_text(encoding="utf-8")
54+
return CONFTEST_MARKER in content
55+
except Exception:
56+
return False
57+
58+
59+
def _backup_existing_conftest(conftest_path: Path) -> Optional[Path]:
60+
"""备份已存在的 conftest.py(如果不是 testsolar 生成的)"""
61+
if not conftest_path.exists():
62+
return None
63+
64+
if _is_testsolar_conftest(conftest_path):
65+
# 是 testsolar 生成的,不需要备份
66+
return None
67+
68+
# 创建备份
69+
backup_path = conftest_path.with_suffix(".py.testsolar_backup")
70+
try:
71+
import shutil
72+
73+
shutil.copy2(conftest_path, backup_path)
74+
logger.info(f"Backed up existing conftest.py to {backup_path}")
75+
return backup_path
76+
except Exception as e:
77+
logger.error(f"Failed to backup conftest.py: {e}")
78+
return None
79+
80+
81+
def _merge_conftest_content(existing_content: str, template_content: str) -> str:
82+
"""合并已有的 conftest.py 内容和模板内容"""
83+
# 如果已有内容中已经包含了我们的标记,说明已经合并过了
84+
if CONFTEST_MARKER in existing_content:
85+
return existing_content
86+
87+
# 将模板内容添加到已有内容的开头
88+
merged = (
89+
template_content + "\n\n# === Original conftest.py content below ===\n\n" + existing_content
90+
)
91+
return merged
92+
93+
94+
def generate_conftest_for_header_injection(project_path: str) -> Optional[str]:
95+
"""
96+
在项目目录中生成支持 header injection 的 conftest.py
97+
98+
Args:
99+
project_path: 项目根目录路径
100+
101+
Returns:
102+
生成的 conftest.py 路径,如果生成失败则返回 None
103+
"""
104+
conftest_path = Path(project_path) / "conftest.py"
105+
106+
try:
107+
if conftest_path.exists():
108+
if _is_testsolar_conftest(conftest_path):
109+
# 已经是 testsolar 生成的,直接覆盖更新
110+
logger.debug(f"Updating existing testsolar conftest.py at {conftest_path}")
111+
conftest_path.write_text(CONFTEST_TEMPLATE, encoding="utf-8")
112+
else:
113+
# 已有用户的 conftest.py,需要合并
114+
logger.info(f"Found existing conftest.py at {conftest_path}, merging content")
115+
existing_content = conftest_path.read_text(encoding="utf-8")
116+
merged_content = _merge_conftest_content(existing_content, CONFTEST_TEMPLATE)
117+
118+
# 备份原文件
119+
_backup_existing_conftest(conftest_path)
120+
121+
# 写入合并后的内容
122+
conftest_path.write_text(merged_content, encoding="utf-8")
123+
logger.info("Merged testsolar header injection into existing conftest.py")
124+
else:
125+
# 不存在 conftest.py,直接创建
126+
logger.info(f"Creating conftest.py at {conftest_path}")
127+
conftest_path.write_text(CONFTEST_TEMPLATE, encoding="utf-8")
128+
129+
return str(conftest_path)
130+
131+
except Exception as e:
132+
logger.error(f"Failed to generate conftest.py: {e}")
133+
return None
134+
135+
136+
def cleanup_generated_conftest(project_path: str) -> None:
137+
"""
138+
清理生成的 conftest.py
139+
140+
如果 conftest.py 是纯粹由 testsolar 生成的,则删除它。
141+
如果是合并的,则恢复原始内容。
142+
143+
Args:
144+
project_path: 项目根目录路径
145+
"""
146+
conftest_path = Path(project_path) / "conftest.py"
147+
backup_path = conftest_path.with_suffix(".py.testsolar_backup")
148+
149+
try:
150+
if backup_path.exists():
151+
# 存在备份,恢复原始文件
152+
import shutil
153+
154+
shutil.move(str(backup_path), str(conftest_path))
155+
logger.info("Restored original conftest.py from backup")
156+
elif conftest_path.exists() and _is_testsolar_conftest(conftest_path):
157+
# 是纯粹由 testsolar 生成的,检查是否只有模板内容
158+
content = conftest_path.read_text(encoding="utf-8")
159+
if "Original conftest.py content below" not in content:
160+
# 没有合并用户内容,可以安全删除
161+
conftest_path.unlink()
162+
logger.info("Removed generated conftest.py")
163+
except Exception as e:
164+
logger.warning(f"Failed to cleanup conftest.py: {e}")

‎pytest/src/testsolar_pytestx/executor.py‎

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@
3737
from .filter import filter_invalid_selector_path
3838
from .parser import parse_case_attributes
3939
from .stream import pytest_main_with_output
40-
from .header_injection import (
41-
initialize_header_injection,
42-
set_current_test_nodeid,
43-
)
40+
from .header_injection import set_current_test_nodeid
41+
from .conftest_generator import generate_conftest_for_header_injection
4442

4543

4644
class RunMode(Enum):
@@ -53,11 +51,6 @@ def should_enable_header_injection() -> bool:
5351
return os.environ.get("ENABLE_API_COLLECTING", "") == "1"
5452

5553

56-
# 主进程初始化(兼容非xdist场景)
57-
# if should_enable_header_injection():
58-
# initialize_header_injection()
59-
60-
6154
class PytestExecutor:
6255
def __init__(
6356
self,
@@ -72,15 +65,6 @@ def __init__(
7265
self.comment_fields = comment_fields
7366
self.data_drive_key = data_drive_key
7467

75-
# def pytest_configure(self, config: Any) -> None:
76-
# """
77-
# 在每个pytest进程(包括xdist的worker进程)启动时调用
78-
# 确保header injection在所有进程中都被初始化
79-
# """
80-
# if should_enable_header_injection():
81-
# logger.info("Initializing header injection in pytest process")
82-
# initialize_header_injection()
83-
8468
def pytest_runtest_logstart(self, nodeid: str, location: Any) -> None:
8569
"""
8670
Called at the start of running the runtest protocol for a single item.
@@ -91,9 +75,9 @@ def pytest_runtest_logstart(self, nodeid: str, location: Any) -> None:
9175
testcase_name = normalize_testcase_name(nodeid, self.data_drive_key)
9276

9377
# 设置当前测试用例的nodeid到上下文
78+
# 注意:这里使用处理后的 testcase_class_name,与 conftest.py 中的格式可能不同
79+
# conftest.py 中使用原始 nodeid,这里使用处理后的名称
9480
if should_enable_header_injection():
95-
logger.info("Initializing header injection in pytest process")
96-
initialize_header_injection()
9781
testcase_class_name = testcase_name.split("?", 1)[-1]
9882
set_current_test_nodeid(testcase_class_name)
9983

@@ -275,6 +259,13 @@ def run_testcases(
275259
if entry.ProjectPath not in sys.path:
276260
sys.path.insert(0, entry.ProjectPath)
277261

262+
# 如果启用了 header injection,生成 conftest.py 以支持 xdist
263+
# 这确保了 xdist worker 进程也能正确初始化 header injection
264+
if should_enable_header_injection():
265+
conftest_path = generate_conftest_for_header_injection(entry.ProjectPath)
266+
if conftest_path:
267+
logger.info(f"Generated conftest.py for header injection at {conftest_path}")
268+
278269
valid_selectors, _ = filter_invalid_selector_path(
279270
workspace=entry.ProjectPath,
280271
selectors=entry.TestSelectors,

‎pytest/src/testsolar_pytestx/header_injection.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _patched_httpx_request(self: Any, method: str, url: str, **kwargs: Any) -> A
153153
nodeid = get_current_test_nodeid()
154154
logger.info("starting patch httpx request...")
155155
if nodeid:
156-
logger.info(f"try to inject nodeid {nodeid} to headers")
156+
logger.debug(f"Injecting nodeid {nodeid} to httpx request headers")
157157
headers = kwargs.get("headers")
158158
kwargs["headers"] = _inject_header_to_dict(headers, nodeid)
159159

@@ -176,7 +176,7 @@ async def _patched_httpx_async_request(
176176
logger.info("starting patch httpx async request...")
177177
nodeid = get_current_test_nodeid()
178178
if nodeid:
179-
logger.info(f"try to inject nodeid {nodeid} to headers")
179+
logger.debug(f"Injecting nodeid {nodeid} to httpx async request headers")
180180
headers = kwargs.get("headers")
181181
kwargs["headers"] = _inject_header_to_dict(headers, nodeid)
182182

‎pytest/testdata/conftest.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Auto-generated conftest.py for testsolar header injection support.
3+
This file ensures that HTTP request header injection works correctly
4+
even when using pytest-xdist for parallel test execution.
5+
6+
DO NOT MODIFY THIS FILE - it will be regenerated on each test run.
7+
"""
8+
import os
9+
10+
# 仅在启用 API 收集时才初始化
11+
if os.environ.get("ENABLE_API_COLLECTING") == "1":
12+
try:
13+
from testsolar_pytestx.header_injection import (
14+
initialize_header_injection,
15+
set_current_test_nodeid,
16+
)
17+
18+
# 在模块导入时就初始化,确保在 httpx 被导入之前完成 patch
19+
initialize_header_injection()
20+
21+
except ImportError:
22+
# testsolar_pytestx 未安装,跳过
23+
pass

‎pytest/testtool.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ schemaVersion: 1.0
22
name: pytest
33
nameZh: pytest自动化测试
44
lang: python
5-
version: '0.1.66'
5+
version: '0.1.67'
66
langType: INTERPRETED
77
description: |-
88
pytest是一个成熟的全功能Python测试工具,可以帮助您编写更好的程序。此测试工具允许您在TestSolar上运行pytest。

‎pytest/uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)