1111import datetime
1212import gc
1313import json
14+ import logging
1415import os
1516import signal
16- import sys
1717import time
1818import warnings
1919from dataclasses import dataclass , field
@@ -1766,15 +1766,21 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
17661766 - Generate index file for all produced fixtures.
17671767 - Create tarball of the output directory if the output is a tarball.
17681768 """
1769+ logger = logging .getLogger ("fill.sessionfinish" )
1770+ is_worker = xdist .is_xdist_worker (session )
1771+
1772+ # Workers collect logs to forward to master via workeroutput
1773+ worker_timing_logs : list [str ] = []
17691774
17701775 def _log_timing (msg : str ) -> None :
1771- """Log with timestamp and flush immediately for CI visibility ."""
1776+ """Log with timestamp. Workers collect logs; master logs directly ."""
17721777 log_line = f"[sessionfinish] { time .strftime ('%H:%M:%S' )} { msg } "
1773- # Print to stderr (unbuffered) for immediate CI visibility
1774- print (log_line , file = sys .stderr , flush = True )
1778+ if is_worker :
1779+ worker_timing_logs .append (log_line )
1780+ else :
1781+ logger .debug (log_line )
17751782
17761783 # Log immediately when hook is entered (before any early returns)
1777- is_worker = xdist .is_xdist_worker (session )
17781784 _log_timing (f"pytest_sessionfinish ENTERED (worker={ is_worker } )" )
17791785
17801786 del exitstatus
@@ -1804,6 +1810,8 @@ def _log_timing(msg: str) -> None:
18041810 # waiting for other workers to finish
18051811 session_instance .pre_alloc_group_builders = None
18061812 gc .collect ()
1813+ # Store timing logs for master to print when this worker finishes
1814+ session .config .workeroutput ["timing_logs" ] = worker_timing_logs # type: ignore[attr-defined] # noqa: E501
18071815
18081816 return
18091817
@@ -1832,6 +1840,8 @@ def _log_timing(msg: str) -> None:
18321840 fc .all_fixtures .clear ()
18331841 fc ._fixtures_to_verify .clear ()
18341842 gc .collect ()
1843+ # Store timing logs for master to print when this worker finishes
1844+ session .config .workeroutput ["timing_logs" ] = worker_timing_logs # type: ignore[attr-defined] # noqa: E501
18351845 return
18361846
18371847 if fixture_output .is_stdout or is_help_or_collectonly_mode (session .config ):
@@ -1848,19 +1858,22 @@ def _log_timing(msg: str) -> None:
18481858 )
18491859
18501860 # Remove any lock files that may have been created.
1851- _log_timing ("Removing lock files..." )
1852- t0 = time .time ()
1853- for file in fixture_output .directory .rglob ("*.lock" ):
1854- file .unlink ()
1855- _log_timing (f"Lock files removed in { time .time () - t0 :.1f} s" )
1861+ lock_files = list (fixture_output .directory .rglob ("*.lock" ))
1862+ if lock_files :
1863+ _log_timing (f"Removing { len (lock_files )} lock files..." )
1864+ t0 = time .time ()
1865+ for file in lock_files :
1866+ file .unlink ()
1867+ _log_timing (f"Lock files removed in { time .time () - t0 :.1f} s" )
18561868
18571869 # Verify fixtures after merge if verification is enabled
1858- _log_timing ("_verify_fixtures_post_merge: starting..." )
1859- t0 = time .time ()
1860- _verify_fixtures_post_merge (session .config , fixture_output .directory )
1861- _log_timing (
1862- f"_verify_fixtures_post_merge: done in { time .time () - t0 :.1f} s"
1863- )
1870+ if session .config .getoption ("verify_fixtures" ):
1871+ _log_timing ("_verify_fixtures_post_merge: starting..." )
1872+ t0 = time .time ()
1873+ _verify_fixtures_post_merge (session .config , fixture_output .directory )
1874+ _log_timing (
1875+ f"_verify_fixtures_post_merge: done in { time .time () - t0 :.1f} s"
1876+ )
18641877
18651878 # Generate index file for all produced fixtures by merging partial indexes.
18661879 # Only merge if partial indexes were actually written (i.e., tests produced
@@ -1880,9 +1893,24 @@ def _log_timing(msg: str) -> None:
18801893 )
18811894
18821895 # Create tarball of the output directory if the output is a tarball.
1883- _log_timing ("create_tarball: starting..." )
1884- t0 = time .time ()
1885- fixture_output .create_tarball ()
1886- _log_timing (f"create_tarball: done in { time .time () - t0 :.1f} s" )
1896+ if fixture_output .is_tarball :
1897+ _log_timing ("create_tarball: starting..." )
1898+ t0 = time .time ()
1899+ fixture_output .create_tarball ()
1900+ _log_timing (f"create_tarball: done in { time .time () - t0 :.1f} s" )
18871901
18881902 _log_timing ("Finalization (master): COMPLETE" )
1903+
1904+
1905+ def pytest_testnodedown (node : Any , error : Any ) -> None :
1906+ """
1907+ Called on master when a worker node finishes.
1908+
1909+ Prints any timing logs collected by the worker during sessionfinish.
1910+ """
1911+ del error
1912+ logger = logging .getLogger ("fill.sessionfinish" )
1913+ worker_id = getattr (node , "workerinput" , {}).get ("workerid" , "unknown" )
1914+ timing_logs = getattr (node , "workeroutput" , {}).get ("timing_logs" , [])
1915+ for log_line in timing_logs :
1916+ logger .debug (f"[worker { worker_id } ] { log_line } " )
0 commit comments