Skip to content

Commit dbe203c

Browse files
committed
Keep track of test suite. Change formatting.
1 parent f17a8f4 commit dbe203c

1 file changed

Lines changed: 75 additions & 6 deletions

File tree

ci/tools/report_universally_skipped_tests.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"test-windows": r"^Test (win-64|windows) / ",
3131
}
3232

33+
INDEX_FILENAME = "job_index.json"
34+
3335
ANSI_ESCAPE = re.compile(r"\x1B\[[0-9;]*[A-Za-z]")
3436
PYTEST_NODE_ID = re.compile(r"tests/\S+\.py::\S+")
3537
PYTEST_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)
4751
class 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

5359
def 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+
125154
def 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

130159
def 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

145183
def 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

Comments
 (0)