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");
4- # you may not use this file except in compliance with the License.
5- # You may obtain a copy of the License at
6- #
7- # http://www.apache.org/licenses/LICENSE-2.0
8- #
9- # Unless required by applicable law or agreed to in writing, software
10- # distributed under the License is distributed on an "AS IS" BASIS,
11- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12- # See the License for the specific language governing permissions and
13- # limitations under the License.
4+
5+ import glob
6+ import os
7+ import time
8+ from typing import Any , Union
149
1510import pytest
11+ from e2e .utils .serving_utils import ( # noqa: E402
12+ FD_API_PORT ,
13+ FD_CACHE_QUEUE_PORT ,
14+ FD_ENGINE_QUEUE_PORT ,
15+ clean_ports ,
16+ )
1617
1718
1819def pytest_configure (config ):
20+ """
21+ Configure pytest:
22+ - Register custom markers
23+ - Ensure log directory exists
24+ """
1925 config .addinivalue_line ("markers" , "gpu: mark test as requiring GPU platform" )
2026
27+ log_dir = os .environ .get ("FD_LOG_DIR" , "log" )
28+ os .makedirs (log_dir , exist_ok = True )
2129
22- def pytest_collection_modifyitems (config , items ):
23- """Skip GPU-marked tests when not on a GPU platform.
2430
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).
31+ def pytest_collection_modifyitems (config , items ):
32+ """
33+ Skip tests marked with 'gpu' if no GPU device is detected.
34+
35+ IMPORTANT:
36+ Do NOT import paddle or fastdeploy here.
37+ This hook runs during test collection (before process fork).
38+ Importing CUDA-related libraries will initialize CUDA runtime,
39+ causing forked subprocesses to fail with:
40+ OSError: CUDA error(3), initialization error.
2941 """
30- import glob
31-
3242 has_gpu = len (glob .glob ("/dev/nvidia[0-9]*" )) > 0
3343
3444 if has_gpu :
@@ -40,18 +50,11 @@ def pytest_collection_modifyitems(config, items):
4050 item .add_marker (skip_marker )
4151
4252
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-
5453class FDRunner :
54+ """
55+ Wrapper for FastDeploy LLM serving process.
56+ """
57+
5558 def __init__ (
5659 self ,
5760 model_name_or_path : str ,
@@ -88,7 +91,9 @@ def generate(
8891 sampling_params ,
8992 ** kwargs : Any ,
9093 ) -> list [tuple [list [list [int ]], list [str ]]]:
91-
94+ """
95+ Run generation and return token IDs and generated texts.
96+ """
9297 req_outputs = self .llm .generate (prompts , sampling_params = sampling_params , ** kwargs )
9398 outputs : list [tuple [list [list [int ]], list [str ]]] = []
9499 for output in req_outputs :
@@ -101,6 +106,9 @@ def generate_topp0(
101106 max_tokens : int ,
102107 ** kwargs : Any ,
103108 ) -> list [tuple [list [int ], str ]]:
109+ """
110+ Generate outputs with deterministic sampling (top_p=0, temperature=0).
111+ """
104112 from fastdeploy .engine .sampling_params import SamplingParams
105113
106114 topp_params = SamplingParams (temperature = 0.0 , top_p = 0 , max_tokens = max_tokens )
@@ -116,4 +124,33 @@ def __exit__(self, exc_type, exc_value, traceback):
116124
117125@pytest .fixture (scope = "session" )
118126def fd_runner ():
127+ """Provide FDRunner as a pytest fixture."""
119128 return FDRunner
129+
130+
131+ @pytest .hookimpl (tryfirst = True , hookwrapper = True )
132+ def pytest_runtest_makereport (item , call ):
133+ """
134+ Capture failed test cases and save error logs to FD_LOG_DIR.
135+
136+ Only logs failures during the test execution phase.
137+ """
138+ outcome = yield
139+ report = outcome .get_result ()
140+
141+ if report .when == "call" and report .failed :
142+ log_dir = os .environ .get ("FD_LOG_DIR" , "log" )
143+ os .makedirs (log_dir , exist_ok = True )
144+
145+ case_name = item .nodeid .split ("::" , 1 )[- 1 ]
146+
147+ error_log_file = os .path .join (log_dir , f"pytest_{ case_name } _error.log" )
148+
149+ with open (error_log_file , "w" , encoding = "utf-8" ) as f :
150+ f .write (f"Case name: { item .nodeid } \n " )
151+ f .write (f"Outcome: { report .outcome } \n " )
152+ f .write (f"Duration: { report .duration :.4f} s\n " )
153+ f .write ("-" * 80 + "\n " )
154+
155+ if report .longrepr :
156+ f .write (str (report .longrepr ))
0 commit comments