Skip to content

Commit 91b8bf2

Browse files
authored
[CI] Add pytest failure log collection and persistence (PaddlePaddle#7405)
1 parent 6ce4854 commit 91b8bf2

3 files changed

Lines changed: 70 additions & 22 deletions

File tree

scripts/coverage_run.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ run_test_with_logging() {
108108
fi
109109

110110
echo ">>> grep error in ${isolated_log_dir}"
111-
grep -Rni --color=auto "error" "${isolated_log_dir}" || true
111+
grep -Rni --color=auto "error" "${isolated_log_dir}" --exclude="pytest_*_error.log" || true
112112
fi
113113

114114
# print all server logs

tests/conftest.py

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
1+
# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -12,23 +12,44 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import glob
16+
import os
17+
import re
18+
import time
19+
from typing import Any, Union
20+
1521
import pytest
22+
from e2e.utils.serving_utils import ( # noqa: E402
23+
FD_API_PORT,
24+
FD_CACHE_QUEUE_PORT,
25+
FD_ENGINE_QUEUE_PORT,
26+
clean_ports,
27+
)
1628

1729

1830
def pytest_configure(config):
31+
"""
32+
Configure pytest:
33+
- Register custom markers
34+
- Ensure log directory exists
35+
"""
1936
config.addinivalue_line("markers", "gpu: mark test as requiring GPU platform")
2037

38+
log_dir = os.environ.get("FD_LOG_DIR", "log")
39+
os.makedirs(log_dir, exist_ok=True)
2140

22-
def pytest_collection_modifyitems(config, items):
23-
"""Skip GPU-marked tests when not on a GPU platform.
2441

25-
IMPORTANT: Do NOT import paddle or fastdeploy here. This function runs
26-
during pytest collection (before fork). Importing paddle initializes the
27-
CUDA runtime, which makes forked child processes unable to re-initialize
28-
CUDA (OSError: CUDA error(3), initialization error).
42+
def pytest_collection_modifyitems(config, items):
43+
"""
44+
Skip tests marked with 'gpu' if no GPU device is detected.
45+
46+
IMPORTANT:
47+
Do NOT import paddle or fastdeploy here.
48+
This hook runs during test collection (before process fork).
49+
Importing CUDA-related libraries will initialize CUDA runtime,
50+
causing forked subprocesses to fail with:
51+
OSError: CUDA error(3), initialization error.
2952
"""
30-
import glob
31-
3253
has_gpu = len(glob.glob("/dev/nvidia[0-9]*")) > 0
3354

3455
if has_gpu:
@@ -40,18 +61,11 @@ def pytest_collection_modifyitems(config, items):
4061
item.add_marker(skip_marker)
4162

4263

43-
import time
44-
from typing import Any, Union
45-
46-
from e2e.utils.serving_utils import ( # noqa: E402
47-
FD_API_PORT,
48-
FD_CACHE_QUEUE_PORT,
49-
FD_ENGINE_QUEUE_PORT,
50-
clean_ports,
51-
)
52-
53-
5464
class FDRunner:
65+
"""
66+
Wrapper for FastDeploy LLM serving process.
67+
"""
68+
5569
def __init__(
5670
self,
5771
model_name_or_path: str,
@@ -88,7 +102,9 @@ def generate(
88102
sampling_params,
89103
**kwargs: Any,
90104
) -> list[tuple[list[list[int]], list[str]]]:
91-
105+
"""
106+
Run generation and return token IDs and generated texts.
107+
"""
92108
req_outputs = self.llm.generate(prompts, sampling_params=sampling_params, **kwargs)
93109
outputs: list[tuple[list[list[int]], list[str]]] = []
94110
for output in req_outputs:
@@ -101,6 +117,9 @@ def generate_topp0(
101117
max_tokens: int,
102118
**kwargs: Any,
103119
) -> list[tuple[list[int], str]]:
120+
"""
121+
Generate outputs with deterministic sampling (top_p=0, temperature=0).
122+
"""
104123
from fastdeploy.engine.sampling_params import SamplingParams
105124

106125
topp_params = SamplingParams(temperature=0.0, top_p=0, max_tokens=max_tokens)
@@ -116,4 +135,33 @@ def __exit__(self, exc_type, exc_value, traceback):
116135

117136
@pytest.fixture(scope="session")
118137
def fd_runner():
138+
"""Provide FDRunner as a pytest fixture."""
119139
return FDRunner
140+
141+
142+
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
143+
def pytest_runtest_makereport(item, call):
144+
"""
145+
Capture failed test cases and save error logs to FD_LOG_DIR.
146+
147+
Only logs failures during the test execution phase.
148+
"""
149+
outcome = yield
150+
report = outcome.get_result()
151+
152+
if report.when == "call" and report.failed:
153+
log_dir = os.environ.get("FD_LOG_DIR", "log")
154+
os.makedirs(log_dir, exist_ok=True)
155+
156+
case_name = re.sub(r"_+", "_", re.sub(r"[^\w\-.]", "_", item.nodeid.split("::", 1)[-1])).strip("_")[:200]
157+
158+
error_log_file = os.path.join(log_dir, f"pytest_{case_name}_error.log")
159+
160+
with open(error_log_file, "w", encoding="utf-8") as f:
161+
f.write(f"Case name: {item.nodeid}\n")
162+
f.write(f"Outcome: {report.outcome}\n")
163+
f.write(f"Duration: {report.duration:.4f}s\n")
164+
f.write("-" * 80 + "\n")
165+
166+
if report.longrepr:
167+
f.write(str(report.longrepr))
File renamed without changes.

0 commit comments

Comments
 (0)