Skip to content

Commit c5bb97c

Browse files
committed
支持请求头注入
1 parent 7931c87 commit c5bb97c

4 files changed

Lines changed: 187 additions & 1 deletion

File tree

‎pytest/CHANGELOG.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Refactor reporter implementation to use FileReporter instead of Reporter
1313

1414
### Fixed
15-
- Improve test case collection and execution with better file handling
15+
- Improve test case collection and execution with better file handling
16+
17+
### Added
18+
- Support for HTTP request header injection during test execution
19+
- Add test case identification to API requests via X-Testsolar-Testcase header

‎pytest/src/testsolar_pytestx/executor.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
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+
)
4044

4145

4246
class RunMode(Enum):
@@ -58,12 +62,23 @@ def __init__(
5862
self.comment_fields = comment_fields
5963
self.data_drive_key = data_drive_key
6064

65+
# 初始化API请求头注入功能,传入判定方法
66+
initialize_header_injection(self._should_enable_header_injection)
67+
68+
def _should_enable_header_injection(self) -> bool:
69+
"""判断是否需要启用请求头注入功能"""
70+
return os.environ.get("ENABLE_API_COLLECTING", "") == "1"
71+
6172
def pytest_runtest_logstart(self, nodeid: str, location: Any) -> None:
6273
"""
6374
Called at the start of running the runtest protocol for a single item.
6475
"""
6576
logger.info(f"{nodeid} start")
6677

78+
# 设置当前测试用例的nodeid到上下文
79+
if self._should_enable_header_injection():
80+
set_current_test_nodeid(nodeid)
81+
6782
# 通知ResultHouse用例开始运行
6883
testcase_name = normalize_testcase_name(nodeid, self.data_drive_key)
6984

@@ -210,6 +225,11 @@ def pytest_runtest_logfinish(self, nodeid: str, location: Any) -> None:
210225

211226
# 上报完成后测试记录就没有用了,删除以节省内存
212227
self.testdata.pop(testcase_name, None)
228+
229+
# 清除当前测试用例的nodeid
230+
if self._should_enable_header_injection():
231+
set_current_test_nodeid(None)
232+
213233
logger.info(f"E {nodeid} runtest_logfinish")
214234

215235
def pytest_sessionfinish(self, session: Session, exitstatus: int) -> None:
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
HTTP请求头注入模块
3+
4+
该模块用于在pytest测试执行过程中,自动向HTTP请求注入测试用例标识信息。
5+
支持requests和httplib/http.client两种HTTP库。
6+
"""
7+
8+
import threading
9+
from typing import Optional, Callable, Any, Dict
10+
11+
from loguru import logger
12+
13+
14+
# ============ 上下文管理 ============
15+
# 用于存储当前测试用例的nodeid
16+
_test_context = threading.local()
17+
18+
# 原始请求方法引用
19+
_original_requests_request: Optional[Callable[..., Any]] = None
20+
_original_httplib_request: Optional[Callable[..., Any]] = None
21+
22+
# 用于存储判定函数的引用
23+
_should_enable_injection_func: Optional[Callable[[], bool]] = None
24+
25+
26+
def get_current_test_nodeid() -> Optional[str]:
27+
"""获取当前测试用例的nodeid"""
28+
return getattr(_test_context, "nodeid", None)
29+
30+
31+
def set_current_test_nodeid(nodeid: Optional[str]) -> None:
32+
"""设置当前测试用例的nodeid"""
33+
_test_context.nodeid = nodeid
34+
35+
36+
# ============ Header 注入逻辑 ============
37+
def _inject_header_to_dict(headers: Optional[Dict[str, Any]], nodeid: str) -> Dict[str, Any]:
38+
"""
39+
向请求头字典注入X-Testsolar-Testcase
40+
41+
Args:
42+
headers: 原始请求头字典
43+
nodeid: 测试用例的nodeid
44+
45+
Returns:
46+
注入后的请求头字典
47+
"""
48+
if headers is None:
49+
headers = {}
50+
elif not isinstance(headers, dict):
51+
# 如果headers不是dict,创建新dict
52+
headers = dict(headers)
53+
else:
54+
# 创建副本避免修改原始dict
55+
headers = headers.copy()
56+
57+
headers["X-Testsolar-Testcase"] = nodeid
58+
return headers
59+
60+
61+
# ============ Monkey Patch 实现 ============
62+
def _patch_requests() -> None:
63+
"""Patch requests库的Session.request方法"""
64+
try:
65+
import requests # type: ignore[import]
66+
67+
global _original_requests_request
68+
69+
if _original_requests_request is not None:
70+
# 已经patch过了
71+
return
72+
73+
_original_requests_request = requests.Session.request
74+
75+
def _patched_request(self: Any, method: str, url: str, **kwargs: Any) -> Any:
76+
# 通过判定函数检查是否需要注入
77+
if _should_enable_injection_func and _should_enable_injection_func():
78+
nodeid = get_current_test_nodeid()
79+
if nodeid:
80+
headers = kwargs.get("headers")
81+
kwargs["headers"] = _inject_header_to_dict(headers, nodeid)
82+
83+
return _original_requests_request(self, method, url, **kwargs)
84+
85+
requests.Session.request = _patched_request
86+
logger.info("Successfully patched requests.Session.request")
87+
88+
except ImportError:
89+
logger.debug("requests library not installed, skip patching")
90+
except Exception as e:
91+
logger.error(f"Failed to patch requests: {e}")
92+
93+
94+
def _patch_httplib() -> None:
95+
"""Patch httplib/http.client的HTTPConnection.request方法"""
96+
try:
97+
# Python 2/3 兼容
98+
try:
99+
import http.client as httplib
100+
except ImportError:
101+
import httplib # type: ignore
102+
103+
global _original_httplib_request
104+
105+
if _original_httplib_request is not None:
106+
# 已经patch过了
107+
return
108+
109+
_original_httplib_request = httplib.HTTPConnection.request
110+
111+
def _patched_httplib_request(
112+
self: Any,
113+
method: str,
114+
url: str,
115+
body: Any = None,
116+
headers: Any = None,
117+
**kwargs: Any,
118+
) -> Any:
119+
# 通过判定函数检查是否需要注入
120+
final_headers: Any = headers
121+
if _should_enable_injection_func and _should_enable_injection_func():
122+
nodeid = get_current_test_nodeid()
123+
if nodeid:
124+
# httplib的headers参数默认是{}而不是None
125+
if final_headers is None:
126+
final_headers = {"X-Testsolar-Testcase": nodeid}
127+
else:
128+
# 创建副本避免修改原始headers
129+
new_headers = dict(final_headers)
130+
new_headers["X-Testsolar-Testcase"] = nodeid
131+
final_headers = new_headers
132+
133+
return _original_httplib_request(self, method, url, body, final_headers, **kwargs)
134+
135+
httplib.HTTPConnection.request = _patched_httplib_request # type: ignore[method-assign]
136+
logger.info("Successfully patched httplib.HTTPConnection.request")
137+
138+
except ImportError:
139+
logger.debug("httplib library not available, skip patching")
140+
except Exception as e:
141+
logger.error(f"Failed to patch httplib: {e}")
142+
143+
144+
# ============ 公共接口 ============
145+
def initialize_header_injection(should_enable_func: Callable[[], bool]) -> None:
146+
"""
147+
初始化HTTP请求头注入功能
148+
149+
Args:
150+
should_enable_func: 判定函数,返回bool表示是否启用注入
151+
"""
152+
global _should_enable_injection_func
153+
_should_enable_injection_func = should_enable_func
154+
155+
logger.info("Initializing API header injection...")
156+
_patch_requests()
157+
_patch_httplib()

‎pytest/tests/test_run.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23
import tempfile
34
from pathlib import Path
45
from unittest import TestCase
@@ -15,6 +16,10 @@
1516
class TestExecuteEntry(TestCase):
1617
testdata_dir = Path(__file__).parent.parent.absolute().joinpath("testdata")
1718

19+
def test_header_injection(self):
20+
os.environ["ENABLE_API_COLLECTING"] = "1"
21+
self.test_run_testcases_from_args()
22+
1823
def test_run_testcases_from_args(self):
1924
with tempfile.TemporaryDirectory() as tmpdir:
2025
report_dir = Path(tmpdir)

0 commit comments

Comments
 (0)