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.
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+
1521import 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
1830def 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-
5464class 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" )
118137def 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 ))
0 commit comments