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" )
@@ -31,6 +43,7 @@ def pytest_addoption(parser: Parser) -> None:
3143 runtime_group = parser .getgroup (name = "Runtime details" )
3244 upgrade_group = parser .getgroup (name = "Upgrade options" )
3345 platform_group = parser .getgroup (name = "Platform" )
46+ must_gather_group = parser .getgroup (name = "MustGather" )
3447 cluster_sanity_group = parser .getgroup (name = "ClusterSanity" )
3548
3649 # AWS config and credentials options
@@ -118,6 +131,12 @@ def pytest_addoption(parser: Parser) -> None:
118131 "--applications-namespace" ,
119132 help = "RHOAI/ODH applications namespace" ,
120133 )
134+ must_gather_group .addoption (
135+ "--collect-must-gather" ,
136+ help = "Indicate if must-gather should be collected on failure." ,
137+ action = "store_true" ,
138+ default = False ,
139+ )
121140
122141 # Cluster sanity options
123142 cluster_sanity_group .addoption (
@@ -205,14 +224,22 @@ def _add_upgrade_test(_item: Item, _upgrade_deployment_modes: list[str]) -> bool
205224
206225
207226def pytest_sessionstart (session : Session ) -> None :
208- tests_log_file = session .config .getoption ("log_file" ) or "pytest-tests.log"
227+ log_file = session .config .getoption ("log_file" ) or "pytest-tests.log"
228+ tests_log_file = os .path .join (get_base_dir (), log_file )
229+ LOGGER .info (f"Writing tests log to { tests_log_file } " )
209230 if os .path .exists (tests_log_file ):
210231 pathlib .Path (tests_log_file ).unlink ()
211-
232+ if session .config .getoption ("--collect-must-gather" ):
233+ session .config .option .must_gather_db = Database ()
212234 session .config .option .log_listener = setup_logging (
213235 log_file = tests_log_file ,
214236 log_level = session .config .getoption ("log_cli_level" ) or logging .INFO ,
215237 )
238+ must_gather_dict = set_must_gather_collector_values ()
239+ shutil .rmtree (
240+ path = must_gather_dict ["must_gather_base_directory" ],
241+ ignore_errors = True ,
242+ )
216243
217244
218245def pytest_fixture_setup (fixturedef : FixtureDef [Any ], request : FixtureRequest ) -> None :
@@ -226,9 +253,23 @@ def pytest_runtest_setup(item: Item) -> None:
226253 2. Adds `fail_if_missing_dependent_operators` fixture for Serverless tests.
227254 3. Adds fixtures to enable KServe/model mesh in DSC for model server tests.
228255 """
229-
230256 BASIC_LOGGER .info (f"\n { separator (symbol_ = '-' , val = item .name )} " )
231257 BASIC_LOGGER .info (f"{ separator (symbol_ = '-' , val = 'SETUP' )} " )
258+ if item .config .getoption ("--collect-must-gather" ):
259+ # set must-gather collection directory:
260+ set_must_gather_collector_directory (item = item , directory_path = get_must_gather_collector_dir ())
261+
262+ # At the begining of setup work, insert current epoch time into the database to indicate test
263+ # start time
264+
265+ try :
266+ db = item .config .option .must_gather_db
267+ db .insert_test_start_time (
268+ test_name = f"{ item .fspath } ::{ item .name } " ,
269+ start_time = int (datetime .datetime .now ().timestamp ()),
270+ )
271+ except Exception as db_exception :
272+ LOGGER .error (f"Database error: { db_exception } . Must-gather collection may not be accurate" )
232273
233274 if KServeDeploymentType .SERVERLESS .lower () in item .keywords :
234275 item .fixturenames .insert (0 , "fail_if_missing_dependent_operators" )
@@ -252,6 +293,10 @@ def pytest_runtest_call(item: Item) -> None:
252293
253294def pytest_runtest_teardown (item : Item ) -> None :
254295 BASIC_LOGGER .info (f"{ separator (symbol_ = '-' , val = 'TEARDOWN' )} " )
296+ # reset must-gather collector after each tests
297+ py_config ["must_gather_collector" ]["collector_directory" ] = py_config ["must_gather_collector" ][
298+ "must_gather_base_directory"
299+ ]
255300
256301
257302def pytest_report_teststatus (report : CollectReport , config : Config ) -> None :
@@ -276,10 +321,56 @@ def pytest_sessionfinish(session: Session, exitstatus: int) -> None:
276321 session .config .option .log_listener .stop ()
277322 if session .config .option .setupplan or session .config .option .collectonly :
278323 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 )
324+ if session .config .getoption ("--collect-must-gather" ):
325+ db = session .config .option .must_gather_db
326+ file_path = db .database_file_path
327+ LOGGER .info (f"Removing database file path { file_path } " )
328+ if os .path .exists (file_path ):
329+ os .remove (file_path )
330+ # clean up the empty folders
331+ collector_directory = py_config ["must_gather_collector" ]["must_gather_base_directory" ]
332+ if os .path .exists (collector_directory ):
333+ for root , dirs , files in os .walk (collector_directory , topdown = False ):
334+ for _dir in dirs :
335+ dir_path = os .path .join (root , _dir )
336+ if not os .listdir (dir_path ):
337+ shutil .rmtree (path = dir_path , ignore_errors = True )
338+ LOGGER .info (f"Deleting pytest base dir { session .config .option .basetemp } " )
339+ shutil .rmtree (path = session .config .option .basetemp , ignore_errors = True )
282340
283341 reporter : Optional [TerminalReporter ] = session .config .pluginmanager .get_plugin ("terminalreporter" )
284342 if reporter :
285343 reporter .summary_stats ()
344+
345+
346+ def calculate_must_gather_timer (test_start_time : int ) -> int :
347+ default_duration = 300
348+ if test_start_time > 0 :
349+ duration = int (datetime .datetime .now ().timestamp ()) - test_start_time
350+ return duration if duration > 60 else default_duration
351+ else :
352+ LOGGER .warning (f"Could not get start time of test. Collecting must-gather for last { default_duration } s" )
353+ return default_duration
354+
355+
356+ def pytest_exception_interact (node : Item | Collector , call : CallInfo [Any ], report : TestReport | CollectReport ) -> None :
357+ LOGGER .error (report .longreprtext )
358+ if node .config .getoption ("--collect-must-gather" ):
359+ test_name = f"{ node .fspath } ::{ node .name } "
360+ LOGGER .info (f"Must-gather collection is enabled for { test_name } ." )
361+
362+ try :
363+ db = node .config .option .must_gather_db
364+ test_start_time = db .get_test_start_time (test_name = test_name )
365+ except Exception as db_exception :
366+ test_start_time = 0
367+ LOGGER .warning (f"Error: { db_exception } in accessing database." )
368+
369+ try :
370+ collect_rhoai_must_gather (
371+ since = calculate_must_gather_timer (test_start_time = test_start_time ),
372+ target_dir = os .path .join (get_must_gather_collector_dir (), "pytest_exception_interact" ),
373+ )
374+
375+ except Exception as current_exception :
376+ LOGGER .warning (f"Failed to collect logs: { test_name } : { current_exception } { traceback .format_exc ()} " )
0 commit comments