ydshieh HF Staff commited on
Commit
0c7f509
Β·
verified Β·
1 Parent(s): f03abac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -57
app.py CHANGED
@@ -1,7 +1,8 @@
1
  import json
2
  import os
 
3
  from functools import lru_cache
4
- from typing import List, Optional, Tuple
5
 
6
  import gradio as gr
7
  from huggingface_hub import HfApi, hf_hub_download
@@ -161,10 +162,130 @@ def _filter_records(repo: str, pr: str, sha: str) -> List[dict]:
161
  records.sort(key=_sort_key, reverse=True)
162
  print(f"DEBUG: Returning {len(records)} records after filtering")
163
  return records[:MAX_ROWS]
164
- return records[:MAX_ROWS]
165
 
166
 
167
- def query(repo: str, pr: str, sha: str) -> Tuple[List[List[str]], str, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  repo = repo.strip()
169
  pr = pr.strip()
170
  sha = sha.strip()
@@ -172,75 +293,180 @@ def query(repo: str, pr: str, sha: str) -> Tuple[List[List[str]], str, str]:
172
  print(f"DEBUG: Query called with repo='{repo}', pr='{pr}', sha='{sha}'")
173
 
174
  if not pr:
175
- return [], json.dumps({"error": "PR number is required."}, indent=2), "Provide a PR number to search."
 
 
 
 
 
 
 
 
176
 
177
  records = _filter_records(repo, pr, sha)
178
  print(f"DEBUG: _filter_records returned {len(records)} records")
179
 
180
  if not records:
181
- return [], json.dumps({"error": "No records found."}, indent=2), f"No records found for PR {pr}."
182
-
183
- table_rows = []
184
- for record in records:
185
- metadata = record.get("metadata") or {}
186
- table_rows.append(
187
- [
188
- metadata.get("collected_at", ""),
189
- metadata.get("repository", ""),
190
- metadata.get("branch", ""),
191
- metadata.get("pull_request_number", ""),
192
- (metadata.get("commit_sha") or "")[:12],
193
- metadata.get("workflow_id", ""),
194
- str(len(record.get("failures", []))),
195
- ]
196
  )
197
 
198
- latest_payload = json.dumps(records[0], indent=2)
199
- status = f"Showing {len(records)} record(s) for PR {pr}."
200
- print(f"DEBUG: Returning {len(table_rows)} table rows")
201
- return table_rows, latest_payload, status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
 
204
  def refresh_dataset() -> str:
205
  _list_collection_files.cache_clear()
206
- return "Cleared cached manifest. Data will be reloaded on next search."
207
-
208
-
209
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  gr.Markdown(
211
  """
212
- # CircleCI Test Collection Helper
213
-
214
- Use the filters below to inspect CircleCI test aggregation records for the Transformers repository (or any
215
- repository that uploads data to the `transformers-community/circleci-test-results` dataset).
216
 
217
- Files are named `failure_summary.json` and organized as `pr-{PR}/sha-{COMMIT}/failure_summary.json`.
 
 
218
  """
219
  )
220
 
221
  with gr.Row():
222
- repo_box = gr.Textbox(label="Repository", placeholder="huggingface/transformers")
223
- pr_box = gr.Textbox(label="PR number (required)")
224
- sha_box = gr.Textbox(label="Commit SHA (prefix accepted)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  with gr.Row():
227
- search_btn = gr.Button("Search")
228
- refresh_btn = gr.Button("Clear cache")
229
-
230
- table = gr.Dataframe(
231
- headers=[
232
- "Collected at",
233
- "Repository",
234
- "Branch",
235
- "PR",
236
- "Commit",
237
- "Workflow ID",
238
- "Failures",
239
- ],
240
- wrap=True,
241
- )
242
- json_view = gr.Code(label="Latest entry details", language="json")
243
- status = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  def get_url_params(request: gr.Request):
246
  """Get URL parameters from the request"""
@@ -261,16 +487,60 @@ with gr.Blocks() as demo:
261
  print(f"DEBUG: Auto-triggering search with repo={repo}, pr={pr}, sha={sha}")
262
  return query(repo, pr, sha)
263
  else:
264
- return [], "", "No PR provided - enter parameters and click Search"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- search_btn.click(query, inputs=[repo_box, pr_box, sha_box], outputs=[table, json_view, status])
267
- refresh_btn.click(refresh_dataset, outputs=status)
268
 
269
  # Load URL parameters when page loads, then auto-search if PR is present
270
- demo.load(get_url_params, outputs=[repo_box, pr_box, sha_box]).then(
 
 
 
271
  auto_search_if_params,
272
  inputs=[repo_box, pr_box, sha_box],
273
- outputs=[table, json_view, status]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  )
275
 
276
  if __name__ == "__main__":
 
1
  import json
2
  import os
3
+ import re
4
  from functools import lru_cache
5
+ from typing import List, Optional, Tuple, Dict
6
 
7
  import gradio as gr
8
  from huggingface_hub import HfApi, hf_hub_download
 
162
  records.sort(key=_sort_key, reverse=True)
163
  print(f"DEBUG: Returning {len(records)} records after filtering")
164
  return records[:MAX_ROWS]
 
165
 
166
 
167
+ def _generate_summary_tables(record: dict) -> Tuple[List[List[str]], List[List[str]]]:
168
+ """Generate by_test and by_model summary tables from a record."""
169
+
170
+ # By test table
171
+ by_test_rows = []
172
+ by_test_data = record.get("by_test", {})
173
+ for test_name, test_info in by_test_data.items():
174
+ count = test_info.get("count", 0)
175
+ errors = test_info.get("errors", {})
176
+ # Format errors as "Nx error_message" for each unique error
177
+ error_list = [f"{cnt}Γ— {err}" for err, cnt in errors.items()]
178
+ error_str = "; ".join(error_list)
179
+ by_test_rows.append([test_name, str(count), error_str])
180
+
181
+ # By model table
182
+ by_model_rows = []
183
+ by_model_data = record.get("by_model", {})
184
+ for model_name, model_info in by_model_data.items():
185
+ count = model_info.get("count", 0)
186
+ errors = model_info.get("errors", {})
187
+ # Format errors as "Nx error_message" for each unique error
188
+ error_list = [f"{cnt}Γ— {err}" for err, cnt in errors.items()]
189
+ error_str = "; ".join(error_list)
190
+ by_model_rows.append([model_name, str(count), error_str])
191
+
192
+ return by_test_rows, by_model_rows
193
+
194
+
195
+ def _generate_markdown_summary(record: dict) -> str:
196
+ """Generate markdown summary for copy-paste to GitHub."""
197
+
198
+ md = "# Failure summary\n\n"
199
+
200
+ # By test section
201
+ md += "## By test\n\n"
202
+ md += "| Test | Failures | Full error(s) |\n"
203
+ md += "| --- | --- | --- |\n"
204
+
205
+ by_test_data = record.get("by_test", {})
206
+ for test_name, test_info in by_test_data.items():
207
+ count = test_info.get("count", 0)
208
+ errors = test_info.get("errors", {})
209
+ error_list = [f"{cnt}Γ— {err}" for err, cnt in errors.items()]
210
+ error_str = "; ".join(error_list)
211
+ md += f"| {test_name} | {count} | {error_str} |\n"
212
+
213
+ # By model section
214
+ md += "\n## By model\n\n"
215
+ md += "| Model | Failures | Full error(s) |\n"
216
+ md += "| --- | --- | --- |\n"
217
+
218
+ by_model_data = record.get("by_model", {})
219
+ for model_name, model_info in by_model_data.items():
220
+ count = model_info.get("count", 0)
221
+ errors = model_info.get("errors", {})
222
+ error_list = [f"{cnt}Γ— {err}" for err, cnt in errors.items()]
223
+ error_str = "; ".join(error_list)
224
+ md += f"| {model_name} | {count} | {error_str} |\n"
225
+
226
+ return md
227
+
228
+
229
+ def _generate_pytest_commands(record: dict) -> str:
230
+ """Generate helpful pytest commands based on the failures."""
231
+
232
+ commands = []
233
+
234
+ by_test_data = record.get("by_test", {})
235
+ by_model_data = record.get("by_model", {})
236
+
237
+ # Add header
238
+ commands.append("# Helpful pytest commands\n")
239
+
240
+ # Commands by test name pattern
241
+ if by_test_data:
242
+ commands.append("## Run specific test patterns:")
243
+ # Extract unique test name patterns (without the variants)
244
+ test_patterns = {} # Use dict to preserve one example per pattern
245
+
246
+ for test_name in by_test_data.keys():
247
+ if "::" in test_name:
248
+ parts = test_name.split("::")
249
+ if len(parts) >= 3:
250
+ # Extract method name without variant suffix
251
+ method = parts[2]
252
+ # Remove _XX_ variant suffixes (like _00_fp16_pad_left_sdpa_kernels)
253
+ method_base = re.sub(r'_\d+_.*$', '', method)
254
+
255
+ # Store the pattern with the original test as example
256
+ if method_base not in test_patterns:
257
+ test_patterns[method_base] = test_name
258
+
259
+ # Generate commands
260
+ for method_base in sorted(test_patterns.keys())[:5]: # Limit to 5 examples
261
+ commands.append(f"```bash\npytest -k {method_base}\n```")
262
+
263
+ # Add a note if there are more patterns
264
+ if len(test_patterns) > 5:
265
+ commands.append(f"\n*...and {len(test_patterns) - 5} more test patterns*")
266
+
267
+ # Commands by model
268
+ if by_model_data:
269
+ commands.append("\n## Run tests for specific models:")
270
+ for model_name in sorted(by_model_data.keys())[:5]: # Limit to 5 examples
271
+ commands.append(f"```bash\npytest tests/models/{model_name}/\n```")
272
+
273
+ # Add a note if there are more models
274
+ if len(by_model_data) > 5:
275
+ commands.append(f"\n*...and {len(by_model_data) - 5} more models*")
276
+
277
+ return "\n".join(commands)
278
+
279
+
280
+ def query(repo: str, pr: str, sha: str) -> Tuple[
281
+ str, # metadata_info
282
+ List[List[str]], # by_test_table
283
+ List[List[str]], # by_model_table
284
+ str, # markdown_summary
285
+ str, # pytest_commands
286
+ str, # raw_json
287
+ str, # status
288
+ ]:
289
  repo = repo.strip()
290
  pr = pr.strip()
291
  sha = sha.strip()
 
293
  print(f"DEBUG: Query called with repo='{repo}', pr='{pr}', sha='{sha}'")
294
 
295
  if not pr:
296
+ return (
297
+ "**Error:** PR number is required.",
298
+ [],
299
+ [],
300
+ "",
301
+ "",
302
+ json.dumps({"error": "PR number is required."}, indent=2),
303
+ "❌ Provide a PR number to search."
304
+ )
305
 
306
  records = _filter_records(repo, pr, sha)
307
  print(f"DEBUG: _filter_records returned {len(records)} records")
308
 
309
  if not records:
310
+ return (
311
+ f"**No records found** for PR {pr}.",
312
+ [],
313
+ [],
314
+ "",
315
+ "",
316
+ json.dumps({"error": "No records found."}, indent=2),
317
+ f"❌ No records found for PR {pr}."
 
 
 
 
 
 
 
318
  )
319
 
320
+ # Use the latest record
321
+ latest_record = records[0]
322
+ metadata = latest_record.get("metadata", {})
323
+
324
+ # Generate metadata info
325
+ metadata_lines = [
326
+ f"**Repository:** {metadata.get('repository', 'N/A')}",
327
+ f"**Branch:** {metadata.get('branch', 'N/A')}",
328
+ f"**PR:** #{metadata.get('pull_request_number', 'N/A')}",
329
+ f"**Commit:** `{metadata.get('commit_sha', 'N/A')[:12]}`",
330
+ f"**Workflow ID:** {metadata.get('workflow_id', 'N/A')}",
331
+ f"**Collected at:** {metadata.get('collected_at', 'N/A')}",
332
+ f"**Total failures:** {len(latest_record.get('failures', []))}",
333
+ ]
334
+ metadata_info = "\n\n".join(metadata_lines)
335
+
336
+ # Generate tables
337
+ by_test_rows, by_model_rows = _generate_summary_tables(latest_record)
338
+
339
+ # Generate markdown summary
340
+ markdown_summary = _generate_markdown_summary(latest_record)
341
+
342
+ # Generate pytest commands
343
+ pytest_commands = _generate_pytest_commands(latest_record)
344
+
345
+ # Raw JSON
346
+ raw_json = json.dumps(latest_record, indent=2)
347
+
348
+ status = f"βœ… Showing latest result from {len(records)} record(s) for PR {pr}."
349
+
350
+ return (
351
+ metadata_info,
352
+ by_test_rows,
353
+ by_model_rows,
354
+ markdown_summary,
355
+ pytest_commands,
356
+ raw_json,
357
+ status
358
+ )
359
 
360
 
361
  def refresh_dataset() -> str:
362
  _list_collection_files.cache_clear()
363
+ return "βœ… Cleared cached manifest. Data will be reloaded on next search."
364
+
365
+
366
+ # Custom CSS for better styling
367
+ custom_css = """
368
+ .metadata-box {
369
+ background-color: #f6f8fa;
370
+ border: 1px solid #d0d7de;
371
+ border-radius: 6px;
372
+ padding: 16px;
373
+ margin: 8px 0;
374
+ }
375
+ .dataframe-container {
376
+ max-height: 500px;
377
+ overflow-y: auto;
378
+ }
379
+ """
380
+
381
+ with gr.Blocks(css=custom_css, title="CircleCI Test Results Viewer") as demo:
382
  gr.Markdown(
383
  """
384
+ # πŸ” CircleCI Test Results Viewer
 
 
 
385
 
386
+ Explore test failure summaries from the Transformers repository CI runs.
387
+
388
+ **Quick start:** Enter a PR number and click Search to see the latest test failures.
389
  """
390
  )
391
 
392
  with gr.Row():
393
+ with gr.Column(scale=1):
394
+ repo_box = gr.Textbox(
395
+ label="Repository",
396
+ placeholder="huggingface/transformers",
397
+ info="Optional: filter by repository name"
398
+ )
399
+ with gr.Column(scale=1):
400
+ pr_box = gr.Textbox(
401
+ label="PR Number",
402
+ placeholder="42240",
403
+ info="Required: PR number to search"
404
+ )
405
+ with gr.Column(scale=1):
406
+ sha_box = gr.Textbox(
407
+ label="Commit SHA",
408
+ placeholder="50947fc",
409
+ info="Optional: commit SHA prefix"
410
+ )
411
 
412
  with gr.Row():
413
+ search_btn = gr.Button("πŸ”Ž Search", variant="primary", scale=2)
414
+ refresh_btn = gr.Button("πŸ”„ Clear Cache", scale=1)
415
+
416
+ status_md = gr.Markdown("")
417
+
418
+ with gr.Tabs() as tabs:
419
+ with gr.Tab("πŸ“Š Summary"):
420
+ metadata_box = gr.Markdown(label="Metadata", elem_classes=["metadata-box"])
421
+
422
+ gr.Markdown("### πŸ“ By Test")
423
+ by_test_table = gr.Dataframe(
424
+ headers=["Test", "Failures", "Full error(s)"],
425
+ wrap=True,
426
+ interactive=False,
427
+ elem_classes=["dataframe-container"],
428
+ )
429
+
430
+ gr.Markdown("### 🏷️ By Model")
431
+ by_model_table = gr.Dataframe(
432
+ headers=["Model", "Failures", "Full error(s)"],
433
+ wrap=True,
434
+ interactive=False,
435
+ elem_classes=["dataframe-container"],
436
+ )
437
+
438
+ with gr.Tab("πŸ“‹ Copy for GitHub"):
439
+ gr.Markdown(
440
+ """
441
+ Copy the markdown below to paste directly into a GitHub comment.
442
+ """
443
+ )
444
+ markdown_output = gr.Textbox(
445
+ label="Markdown Summary",
446
+ lines=20,
447
+ max_lines=30,
448
+ show_copy_button=True,
449
+ )
450
+
451
+ with gr.Tab("πŸ§ͺ Pytest Commands"):
452
+ gr.Markdown(
453
+ """
454
+ Helpful pytest commands to run specific failing tests locally.
455
+ """
456
+ )
457
+ pytest_output = gr.Markdown()
458
+
459
+ with gr.Tab("πŸ”§ Raw JSON"):
460
+ gr.Markdown(
461
+ """
462
+ Full JSON data for debugging or custom processing.
463
+ """
464
+ )
465
+ json_view = gr.Code(
466
+ label="Latest entry details",
467
+ language="json",
468
+ lines=20,
469
+ )
470
 
471
  def get_url_params(request: gr.Request):
472
  """Get URL parameters from the request"""
 
487
  print(f"DEBUG: Auto-triggering search with repo={repo}, pr={pr}, sha={sha}")
488
  return query(repo, pr, sha)
489
  else:
490
+ return (
491
+ "Enter a PR number and click Search",
492
+ [],
493
+ [],
494
+ "",
495
+ "",
496
+ "",
497
+ "πŸ’‘ Enter a PR number above to get started"
498
+ )
499
+
500
+ # Connect the search button
501
+ search_btn.click(
502
+ query,
503
+ inputs=[repo_box, pr_box, sha_box],
504
+ outputs=[
505
+ metadata_box,
506
+ by_test_table,
507
+ by_model_table,
508
+ markdown_output,
509
+ pytest_output,
510
+ json_view,
511
+ status_md
512
+ ]
513
+ )
514
 
515
+ # Connect the refresh button
516
+ refresh_btn.click(refresh_dataset, outputs=status_md)
517
 
518
  # Load URL parameters when page loads, then auto-search if PR is present
519
+ demo.load(
520
+ get_url_params,
521
+ outputs=[repo_box, pr_box, sha_box]
522
+ ).then(
523
  auto_search_if_params,
524
  inputs=[repo_box, pr_box, sha_box],
525
+ outputs=[
526
+ metadata_box,
527
+ by_test_table,
528
+ by_model_table,
529
+ markdown_output,
530
+ pytest_output,
531
+ json_view,
532
+ status_md
533
+ ]
534
+ )
535
+
536
+ gr.Markdown(
537
+ """
538
+ ---
539
+
540
+ **Data source:** [transformers-community/circleci-test-results](https://huggingface.co/datasets/transformers-community/circleci-test-results)
541
+
542
+ Files are organized as `pr-{PR}/sha-{COMMIT}/failure_summary.json`
543
+ """
544
  )
545
 
546
  if __name__ == "__main__":