3030 "test-windows" : r"^Test (win-64|windows) / " ,
3131}
3232
33+ INDEX_FILENAME = "job_index.json"
34+
3335ANSI_ESCAPE = re .compile (r"\x1B\[[0-9;]*[A-Za-z]" )
3436PYTEST_NODE_ID = re .compile (r"tests/\S+\.py::\S+" )
3537PYTEST_TEST_OUTCOME = re .compile (r"(tests/\S+\.py::\S+)\s+(PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)\b" )
@@ -41,13 +43,17 @@ class ConfigResult:
4143 job_ids : list [int ]
4244 skipped : set [str ]
4345 has_logs : bool
46+ # test_id -> suite name (e.g. "cuda_bindings"), empty string if unknown
47+ test_suites : dict [str , str ] = dataclasses .field (default_factory = dict )
4448
4549
4650@dataclasses .dataclass (frozen = True )
4751class ConfigLogs :
4852 name : str
4953 job_ids : list [int ]
5054 log_paths : list [Path ]
55+ # job_id -> suite name extracted from the job name
56+ job_names : dict [int , str ] = dataclasses .field (default_factory = dict )
5157
5258
5359def run_gh (* args : str , check : bool = True ) -> subprocess .CompletedProcess [str ]:
@@ -122,42 +128,88 @@ def extract_test_status_sets(text: str) -> tuple[set[str], set[str]]:
122128 return skipped , non_skipped
123129
124130
131+ def extract_suite_name (job_name : str , config_name : str ) -> str :
132+ """Return the test suite portion of a job name (first word after the config prefix)."""
133+ pattern = CONFIG_PATTERNS .get (config_name , "" )
134+ if pattern :
135+ match = re .match (pattern , job_name )
136+ if match :
137+ remainder = job_name [match .end () :]
138+ parts = remainder .split ()
139+ return parts [0 ] if parts else job_name
140+ return job_name
141+
142+
143+ def save_job_index (logs_root : Path , index : dict [str , dict [str , str ]]) -> None :
144+ (logs_root / INDEX_FILENAME ).write_text (json .dumps (index , indent = 2 ), encoding = "utf-8" )
145+
146+
147+ def load_job_index (logs_root : Path ) -> dict [str , dict [str , str ]]:
148+ index_path = logs_root / INDEX_FILENAME
149+ if index_path .exists ():
150+ return json .loads (index_path .read_text (encoding = "utf-8" ))
151+ return {}
152+
153+
125154def match_job_ids (jobs : Iterable [dict ], pattern : str ) -> list [int ]:
126155 regex = re .compile (pattern )
127156 return [int (job ["id" ]) for job in jobs if regex .search (str (job .get ("name" , "" )))]
128157
129158
130159def discover_config_logs (logs_root : Path ) -> list [ConfigLogs ]:
131160 configs : list [ConfigLogs ] = []
161+ index = load_job_index (logs_root )
132162
133163 for config in CONFIG_PATTERNS :
134164 config_dir = logs_root / config
135165 log_paths = sorted (config_dir .glob ("*.log" )) if config_dir .exists () else []
136166 job_ids : list [int ] = []
167+ job_names : dict [int , str ] = {}
168+ config_index = index .get (config , {})
169+
137170 for log_path in log_paths :
138171 with contextlib .suppress (ValueError ):
139- job_ids .append (int (log_path .stem ))
140- configs .append (ConfigLogs (name = config , job_ids = job_ids , log_paths = log_paths ))
172+ job_id = int (log_path .stem )
173+ job_ids .append (job_id )
174+ suite = config_index .get (str (job_id ), "" )
175+ if suite :
176+ job_names [job_id ] = suite
177+
178+ configs .append (ConfigLogs (name = config , job_ids = job_ids , log_paths = log_paths , job_names = job_names ))
141179
142180 return configs
143181
144182
145183def download_config_logs (jobs : list [dict ], repo : str , run_id : str , logs_root : Path ) -> list [ConfigLogs ]:
146184 configs : list [ConfigLogs ] = []
185+ index : dict [str , dict [str , str ]] = {}
147186
148187 for config , pattern in CONFIG_PATTERNS .items ():
149188 config_dir = logs_root / config
150189 job_ids = match_job_ids (jobs , pattern )
151190 log_paths : list [Path ] = []
152191
192+ # Build job_id -> suite_name from job metadata before downloading logs.
193+ regex = re .compile (pattern )
194+ job_names : dict [int , str ] = {}
195+ for job in jobs :
196+ job_name = str (job .get ("name" , "" ))
197+ if not regex .search (job_name ):
198+ continue
199+ job_id = int (job ["id" ])
200+ if job_id in job_ids :
201+ job_names [job_id ] = extract_suite_name (job_name , config )
202+
153203 for job_id in job_ids :
154204 log_path = config_dir / f"{ job_id } .log"
155205 if not log_path .exists () and not download_job_log (repo , run_id , job_id , log_path ):
156206 continue
157207 log_paths .append (log_path )
158208
159- configs .append (ConfigLogs (name = config , job_ids = job_ids , log_paths = log_paths ))
209+ configs .append (ConfigLogs (name = config , job_ids = job_ids , log_paths = log_paths , job_names = job_names ))
210+ index [config ] = {str (jid ): name for jid , name in job_names .items ()}
160211
212+ save_job_index (logs_root , index )
161213 return configs
162214
163215
@@ -167,13 +219,23 @@ def analyze_config_logs(config_logs: list[ConfigLogs]) -> list[ConfigResult]:
167219 for config in config_logs :
168220 skipped_any : set [str ] = set ()
169221 non_skipped_any : set [str ] = set ()
222+ test_suites : dict [str , str ] = {}
223+
170224 for log_path in config .log_paths :
171225 text = log_path .read_text (encoding = "utf-8" , errors = "replace" )
172226
173227 skipped_in_log , non_skipped_in_log = extract_test_status_sets (text )
174228 skipped_any .update (skipped_in_log )
175229 non_skipped_any .update (non_skipped_in_log )
176230
231+ # Associate skipped test IDs with the suite derived from the job name.
232+ with contextlib .suppress (ValueError ):
233+ job_id = int (log_path .stem )
234+ suite = config .job_names .get (job_id , "" )
235+ if suite :
236+ for test_id in skipped_in_log :
237+ test_suites .setdefault (test_id , suite )
238+
177239 # For sharded matrices, a test may only appear in one log. Treat it as
178240 # config-skipped if it is skipped at least once and never non-skipped
179241 # (passed/failed/error/xpass/xfail) in that config.
@@ -185,6 +247,7 @@ def analyze_config_logs(config_logs: list[ConfigLogs]) -> list[ConfigResult]:
185247 job_ids = config .job_ids ,
186248 skipped = skipped_for_config ,
187249 has_logs = bool (config .log_paths ),
250+ test_suites = test_suites ,
188251 )
189252 )
190253
@@ -217,16 +280,22 @@ def build_summary(results: list[ConfigResult]) -> str:
217280 "_Note: the test `tests/test_cuda.py::test_always_skip` is expected to be skipped in all configurations, but is missing._"
218281 )
219282
283+ # Merge test->suite mappings across all configs (first one seen wins).
284+ test_suites : dict [str , str ] = {}
285+ for result in results :
286+ for test_id , suite in result .test_suites .items ():
287+ test_suites .setdefault (test_id , suite )
288+
220289 universal = sorted (intersection or set ())
221290 lines .append (f"Tests skipped across wheel test configurations ({ len (results )} ):" )
222291 lines .append ("" )
223292 if not universal :
224293 lines .append ("_No tests were skipped in all configurations._" )
225294 else :
226- lines .append ("| Test |" )
227- lines .append ("| --- |" )
228295 for test in universal :
229- lines .append (f"| `{ test } ` |" )
296+ suite = test_suites .get (test , "" )
297+ label = f"{ suite } /{ test } " if suite else test
298+ lines .append (f"- [ ] `{ label } `" )
230299
231300 return "\n " .join (lines ) + "\n "
232301
0 commit comments