Skip to content

Commit eba6fce

Browse files
author
William Yang
committed
feat(submit): add polling for evaluation result
- Add --async flag to skip waiting for result - Add --timeout flag to customize wait time (default: 60s) - Show spinner with elapsed time while polling - Display detailed test results after evaluation - Update README with new submit options
1 parent 391afad commit eba6fce

File tree

2 files changed

+179
-5
lines changed

2 files changed

+179
-5
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,45 @@ bootcs check cs50/hello --log
9595
bootcs submit cs50/hello
9696
```
9797

98-
系统会显示要提交的文件列表,确认后上传。
98+
系统会显示要提交的文件列表,确认后上传,并**自动等待评测结果**
99+
100+
```
101+
📦 Submitting cs50/hello
102+
103+
Files to submit:
104+
• hello.c
105+
106+
Submit these files? [Y/n] Y
107+
Submitting...
108+
109+
✅ Submitted successfully!
110+
Submission ID: cmj9tcg3p00kfi7z4ih3l6quz
111+
Short Hash: f3b2fac3
112+
113+
⏳ Evaluating... ⠹ (3s)
114+
115+
🎉 Evaluation Complete!
116+
117+
Status: SUCCESS
118+
Passed: 4/4
119+
120+
✅ file_exists
121+
✅ compiles
122+
✅ emma
123+
✅ rodrigo
124+
```
125+
126+
### 提交选项
99127

100128
```bash
101129
# 跳过确认,直接提交
102130
bootcs submit cs50/hello -y
131+
132+
# 异步模式:提交后立即返回,不等待结果
133+
bootcs submit cs50/hello --async
134+
135+
# 自定义超时时间(默认 60 秒)
136+
bootcs submit cs50/hello --timeout 120
103137
```
104138

105139
## 📋 常用命令速查

bootcs/__main__.py

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import json
1111
import os
1212
import sys
13+
import time
1314
from pathlib import Path
1415

1516
import termcolor
@@ -51,6 +52,10 @@ def main():
5152
submit_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")
5253
submit_parser.add_argument("-L", "--language", help="Language of submission (auto-detected if not specified)")
5354
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)")
5459

5560
# Auth commands
5661
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):
407412
return None
408413

409414

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+
410524
def run_submit(args):
411525
"""Run the submit command."""
412526
from .auth import is_logged_in, get_token
@@ -514,14 +628,40 @@ def run_submit(args):
514628

515629
print(f" Submission ID: {result.submission_id}")
516630
print(f" Short Hash: {result.short_hash}")
517-
print(f" Status: {result.status}")
518631

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")
520649
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")
522652
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
523656

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
525665

526666
except APIError as e:
527667
print()

0 commit comments

Comments
 (0)