|
10 | 10 | import json |
11 | 11 | import os |
12 | 12 | import sys |
| 13 | +import time |
13 | 14 | from pathlib import Path |
14 | 15 |
|
15 | 16 | import termcolor |
@@ -51,6 +52,10 @@ def main(): |
51 | 52 | submit_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation") |
52 | 53 | submit_parser.add_argument("-L", "--language", help="Language of submission (auto-detected if not specified)") |
53 | 54 | submit_parser.add_argument("--local", metavar="PATH", help="Path to local checks directory (for file list)") |
| 55 | + submit_parser.add_argument("--async", dest="async_mode", action="store_true", |
| 56 | + help="Don't wait for evaluation result (return immediately)") |
| 57 | + submit_parser.add_argument("--timeout", type=int, default=60, |
| 58 | + help="Timeout in seconds to wait for evaluation (default: 60)") |
54 | 59 |
|
55 | 60 | # Auth commands |
56 | 61 | subparsers.add_parser("login", help="Log in with GitHub") |
@@ -407,6 +412,115 @@ def find_check_dir(slug, language: str = "c", force_update: bool = False): |
407 | 412 | return None |
408 | 413 |
|
409 | 414 |
|
| 415 | +# Spinner characters for polling animation |
| 416 | +SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] |
| 417 | + |
| 418 | + |
| 419 | +def wait_for_result(submission_id: str, token: str, timeout: int = 60): |
| 420 | + """ |
| 421 | + Poll for submission evaluation result. |
| 422 | + |
| 423 | + Args: |
| 424 | + submission_id: The submission ID to poll. |
| 425 | + token: Authentication token. |
| 426 | + timeout: Maximum time to wait in seconds. |
| 427 | + |
| 428 | + Returns: |
| 429 | + Submission result dict, or None if timeout. |
| 430 | + """ |
| 431 | + from .api.client import APIClient, APIError |
| 432 | + |
| 433 | + client = APIClient(token=token) |
| 434 | + start_time = time.time() |
| 435 | + poll_count = 0 |
| 436 | + |
| 437 | + while True: |
| 438 | + elapsed = time.time() - start_time |
| 439 | + |
| 440 | + # Timeout check |
| 441 | + if elapsed > timeout: |
| 442 | + return None |
| 443 | + |
| 444 | + # Show spinner |
| 445 | + spinner_char = SPINNER[poll_count % len(SPINNER)] |
| 446 | + elapsed_int = int(elapsed) |
| 447 | + sys.stdout.write(f"\r⏳ Evaluating... {spinner_char} ({elapsed_int}s) ") |
| 448 | + sys.stdout.flush() |
| 449 | + |
| 450 | + try: |
| 451 | + result = client.get(f"/api/submissions/{submission_id}") |
| 452 | + status = result.get('status') |
| 453 | + |
| 454 | + # Terminal states |
| 455 | + if status in ['SUCCESS', 'FAILURE', 'ERROR', 'TIMEOUT']: |
| 456 | + sys.stdout.write("\r" + " " * 40 + "\r") # Clear line |
| 457 | + sys.stdout.flush() |
| 458 | + return result |
| 459 | + except APIError: |
| 460 | + pass # Continue polling on error |
| 461 | + |
| 462 | + poll_count += 1 |
| 463 | + # First 5 polls: 1s interval (fast feedback) |
| 464 | + # After that: 2s interval (reduce load) |
| 465 | + interval = 1 if poll_count <= 5 else 2 |
| 466 | + time.sleep(interval) |
| 467 | + |
| 468 | + |
| 469 | +def display_result(result: dict): |
| 470 | + """ |
| 471 | + Display evaluation result. |
| 472 | + |
| 473 | + Args: |
| 474 | + result: Submission result dict from API. |
| 475 | + """ |
| 476 | + status = result.get('status') |
| 477 | + eval_result = result.get('result', {}) |
| 478 | + test_results = eval_result.get('results', []) |
| 479 | + passed = sum(1 for r in test_results if r.get('passed')) |
| 480 | + total = len(test_results) |
| 481 | + |
| 482 | + print() |
| 483 | + if status == 'SUCCESS': |
| 484 | + termcolor.cprint("🎉 Evaluation Complete!", "green", attrs=["bold"]) |
| 485 | + print() |
| 486 | + termcolor.cprint(f" Status: SUCCESS", "green") |
| 487 | + elif status == 'FAILURE': |
| 488 | + termcolor.cprint("❌ Some tests failed", "red", attrs=["bold"]) |
| 489 | + print() |
| 490 | + termcolor.cprint(f" Status: FAILURE", "red") |
| 491 | + elif status == 'ERROR': |
| 492 | + termcolor.cprint("⚠️ Evaluation error", "yellow", attrs=["bold"]) |
| 493 | + print() |
| 494 | + termcolor.cprint(f" Status: ERROR", "yellow") |
| 495 | + elif status == 'TIMEOUT': |
| 496 | + termcolor.cprint("⏰ Evaluation timeout", "yellow", attrs=["bold"]) |
| 497 | + print() |
| 498 | + termcolor.cprint(f" Status: TIMEOUT", "yellow") |
| 499 | + |
| 500 | + if total > 0: |
| 501 | + color = "green" if passed == total else "red" |
| 502 | + termcolor.cprint(f" Passed: {passed}/{total}", color) |
| 503 | + |
| 504 | + # Show individual test results |
| 505 | + if test_results: |
| 506 | + print() |
| 507 | + for r in test_results: |
| 508 | + name = r.get('name', 'unknown') |
| 509 | + is_passed = r.get('passed', False) |
| 510 | + description = r.get('description', '') |
| 511 | + |
| 512 | + if is_passed: |
| 513 | + icon = termcolor.colored('✅', 'green') |
| 514 | + else: |
| 515 | + icon = termcolor.colored('❌', 'red') |
| 516 | + |
| 517 | + if description: |
| 518 | + print(f" {icon} {name} - {description}") |
| 519 | + else: |
| 520 | + print(f" {icon} {name}") |
| 521 | + print() |
| 522 | + |
| 523 | + |
410 | 524 | def run_submit(args): |
411 | 525 | """Run the submit command.""" |
412 | 526 | from .auth import is_logged_in, get_token |
@@ -514,14 +628,40 @@ def run_submit(args): |
514 | 628 |
|
515 | 629 | print(f" Submission ID: {result.submission_id}") |
516 | 630 | print(f" Short Hash: {result.short_hash}") |
517 | | - print(f" Status: {result.status}") |
518 | 631 |
|
519 | | - if result.status == "EVALUATING": |
| 632 | + # If async mode or not evaluating, just show status and return |
| 633 | + if args.async_mode or result.status != "EVALUATING": |
| 634 | + print(f" Status: {result.status}") |
| 635 | + if result.status == "EVALUATING": |
| 636 | + print() |
| 637 | + termcolor.cprint("💡 Your code is being evaluated. Check results at:", "cyan") |
| 638 | + print(f" https://bootcs.dev/submissions/{result.submission_id}") |
| 639 | + return 0 |
| 640 | + |
| 641 | + # Wait for evaluation result (polling mode) |
| 642 | + print() |
| 643 | + eval_result = wait_for_result(result.submission_id, token, timeout=args.timeout) |
| 644 | + |
| 645 | + if eval_result is None: |
| 646 | + # Timeout |
| 647 | + print() |
| 648 | + termcolor.cprint(f"⏰ Evaluation taking longer than expected ({args.timeout}s)", "yellow") |
520 | 649 | print() |
521 | | - termcolor.cprint("💡 Your code is being evaluated. Check results at:", "cyan") |
| 650 | + print(" Your submission is still being processed.") |
| 651 | + termcolor.cprint(" Check results at:", "cyan") |
522 | 652 | print(f" https://bootcs.dev/submissions/{result.submission_id}") |
| 653 | + print() |
| 654 | + termcolor.cprint(f" Or wait longer with: bootcs submit {slug} --timeout {args.timeout * 2}", "white") |
| 655 | + return 0 |
523 | 656 |
|
524 | | - return 0 |
| 657 | + # Display final result |
| 658 | + display_result(eval_result) |
| 659 | + |
| 660 | + # Return exit code based on result |
| 661 | + if eval_result.get('status') == 'SUCCESS': |
| 662 | + return 0 |
| 663 | + else: |
| 664 | + return 1 |
525 | 665 |
|
526 | 666 | except APIError as e: |
527 | 667 | print() |
|
0 commit comments