22import os
33import pathlib
44import shutil
5+ import datetime
6+ import traceback
57
68import shortuuid
9+ from _pytest .runner import CallInfo
10+ from _pytest .reports import TestReport
711from pytest import (
812 Parser ,
913 Session ,
1014 FixtureRequest ,
1115 FixtureDef ,
1216 Item ,
17+ Collector ,
1318 Config ,
1419 CollectReport ,
1520)
1823from pytest_testconfig import config as py_config
1924
2025from utilities .constants import KServeDeploymentType
26+ from utilities .database import Database
2127from utilities .logger import separator , setup_logging
22-
28+ from utilities .must_gather_collector import (
29+ set_must_gather_collector_directory ,
30+ set_must_gather_collector_values ,
31+ get_must_gather_collector_dir ,
32+ collect_rhoai_must_gather ,
33+ get_base_dir ,
34+ )
2335
2436LOGGER = logging .getLogger (name = __name__ )
2537BASIC_LOGGER = logging .getLogger (name = "basic" )
@@ -30,7 +42,7 @@ def pytest_addoption(parser: Parser) -> None:
3042 buckets_group = parser .getgroup (name = "Buckets" )
3143 runtime_group = parser .getgroup (name = "Runtime details" )
3244 upgrade_group = parser .getgroup (name = "Upgrade options" )
33- platform_group = parser .getgroup (name = "Platform " )
45+ must_gather_group = parser .getgroup (name = "MustGather " )
3446 cluster_sanity_group = parser .getgroup (name = "ClusterSanity" )
3547
3648 # AWS config and credentials options
@@ -112,11 +124,11 @@ def pytest_addoption(parser: Parser) -> None:
112124 help = "Coma-separated str; specify inference service deployment modes tests to run in upgrade tests. "
113125 "If not set, all will be tested." ,
114126 )
115-
116- # Platform options
117- platform_group . addoption (
118- "--applications-namespace " ,
119- help = "RHOAI/ODH applications namespace" ,
127+ must_gather_group . addoption (
128+ "--collect-must-gather" ,
129+ help = "Indicate if must-gather should be collected on failure." ,
130+ action = "store_true " ,
131+ default = False ,
120132 )
121133
122134 # Cluster sanity options
@@ -205,14 +217,22 @@ def _add_upgrade_test(_item: Item, _upgrade_deployment_modes: list[str]) -> bool
205217
206218
207219def pytest_sessionstart (session : Session ) -> None :
208- tests_log_file = session .config .getoption ("log_file" ) or "pytest-tests.log"
220+ log_file = session .config .getoption ("log_file" ) or "pytest-tests.log"
221+ tests_log_file = os .path .join (get_base_dir (), log_file )
222+ LOGGER .info (f"Writing tests log to { tests_log_file } " )
209223 if os .path .exists (tests_log_file ):
210224 pathlib .Path (tests_log_file ).unlink ()
211-
225+ if session .config .getoption ("--collect-must-gather" ):
226+ session .config .option .must_gather_db = Database ()
212227 session .config .option .log_listener = setup_logging (
213228 log_file = tests_log_file ,
214229 log_level = session .config .getoption ("log_cli_level" ) or logging .INFO ,
215230 )
231+ must_gather_dict = set_must_gather_collector_values ()
232+ shutil .rmtree (
233+ path = must_gather_dict ["must_gather_base_directory" ],
234+ ignore_errors = True ,
235+ )
216236
217237
218238def pytest_fixture_setup (fixturedef : FixtureDef [Any ], request : FixtureRequest ) -> None :
@@ -226,9 +246,23 @@ def pytest_runtest_setup(item: Item) -> None:
226246 2. Adds `fail_if_missing_dependent_operators` fixture for Serverless tests.
227247 3. Adds fixtures to enable KServe/model mesh in DSC for model server tests.
228248 """
229-
230249 BASIC_LOGGER .info (f"\n { separator (symbol_ = '-' , val = item .name )} " )
231250 BASIC_LOGGER .info (f"{ separator (symbol_ = '-' , val = 'SETUP' )} " )
251+ if item .config .getoption ("--collect-must-gather" ):
252+ # set must-gather collection directory:
253+ set_must_gather_collector_directory (item = item , directory_path = get_must_gather_collector_dir ())
254+
255+ # At the begining of setup work, insert current epoch time into the database to indicate test
256+ # start time
257+
258+ try :
259+ db = item .config .option .must_gather_db
260+ db .insert_test_start_time (
261+ test_name = f"{ item .fspath } ::{ item .name } " ,
262+ start_time = int (datetime .datetime .now ().timestamp ()),
263+ )
264+ except Exception as db_exception :
265+ LOGGER .error (f"Database error: { db_exception } . Must-gather collection may not be accurate" )
232266
233267 if KServeDeploymentType .SERVERLESS .lower () in item .keywords :
234268 item .fixturenames .insert (0 , "fail_if_missing_dependent_operators" )
@@ -252,6 +286,10 @@ def pytest_runtest_call(item: Item) -> None:
252286
253287def pytest_runtest_teardown (item : Item ) -> None :
254288 BASIC_LOGGER .info (f"{ separator (symbol_ = '-' , val = 'TEARDOWN' )} " )
289+ # reset must-gather collector after each tests
290+ py_config ["must_gather_collector" ]["collector_directory" ] = py_config ["must_gather_collector" ][
291+ "must_gather_base_directory"
292+ ]
255293
256294
257295def pytest_report_teststatus (report : CollectReport , config : Config ) -> None :
@@ -276,10 +314,56 @@ def pytest_sessionfinish(session: Session, exitstatus: int) -> None:
276314 session .config .option .log_listener .stop ()
277315 if session .config .option .setupplan or session .config .option .collectonly :
278316 return
279- base_dir = py_config ["tmp_base_dir" ]
280- LOGGER .info (f"Deleting pytest base dir { base_dir } " )
281- shutil .rmtree (path = base_dir , ignore_errors = True )
317+ if session .config .getoption ("--collect-must-gather" ):
318+ db = session .config .option .must_gather_db
319+ file_path = db .database_file_path
320+ LOGGER .info (f"Removing database file path { file_path } " )
321+ if os .path .exists (file_path ):
322+ os .remove (file_path )
323+ # clean up the empty folders
324+ collector_directory = py_config ["must_gather_collector" ]["must_gather_base_directory" ]
325+ if os .path .exists (collector_directory ):
326+ for root , dirs , files in os .walk (collector_directory , topdown = False ):
327+ for _dir in dirs :
328+ dir_path = os .path .join (root , _dir )
329+ if not os .listdir (dir_path ):
330+ shutil .rmtree (path = dir_path , ignore_errors = True )
331+ LOGGER .info (f"Deleting pytest base dir { session .config .option .basetemp } " )
332+ shutil .rmtree (path = session .config .option .basetemp , ignore_errors = True )
282333
283334 reporter : Optional [TerminalReporter ] = session .config .pluginmanager .get_plugin ("terminalreporter" )
284335 if reporter :
285336 reporter .summary_stats ()
337+
338+
339+ def calculate_must_gather_timer (test_start_time : int ) -> int :
340+ default_duration = 300
341+ if test_start_time > 0 :
342+ duration = int (datetime .datetime .now ().timestamp ()) - test_start_time
343+ return duration if duration > 60 else default_duration
344+ else :
345+ LOGGER .warning (f"Could not get start time of test. Collecting must-gather for last { default_duration } s" )
346+ return default_duration
347+
348+
349+ def pytest_exception_interact (node : Item | Collector , call : CallInfo [Any ], report : TestReport | CollectReport ) -> None :
350+ LOGGER .error (report .longreprtext )
351+ if node .config .getoption ("--collect-must-gather" ):
352+ test_name = f"{ node .fspath } ::{ node .name } "
353+ LOGGER .info (f"Must-gather collection is enabled for { test_name } ." )
354+
355+ try :
356+ db = node .config .option .must_gather_db
357+ test_start_time = db .get_test_start_time (test_name = test_name )
358+ except Exception as db_exception :
359+ test_start_time = 0
360+ LOGGER .warning (f"Error: { db_exception } in accessing database." )
361+
362+ try :
363+ collect_rhoai_must_gather (
364+ since = calculate_must_gather_timer (test_start_time = test_start_time ),
365+ target_dir = os .path .join (get_must_gather_collector_dir (), "pytest_exception_interact" ),
366+ )
367+
368+ except Exception as current_exception :
369+ LOGGER .warning (f"Failed to collect logs: { test_name } : { current_exception } { traceback .format_exc ()} " )
0 commit comments