diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 45d9163..045e14c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,9 +3,8 @@ name: Deploy to dev on: push: branches: - - deploy/add-email-sender - dev - - feat/api-SandBox + - feat/finish_all_submission_function concurrency: deploy-dev diff --git a/submissions/migrations/0004_alter_submissionresult_status_and_more.py b/submissions/migrations/0004_alter_submissionresult_status_and_more.py new file mode 100644 index 0000000..d6c3d10 --- /dev/null +++ b/submissions/migrations/0004_alter_submissionresult_status_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-12-28 11:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0003_remove_editorial_editorials_problem_79b740_idx_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='submissionresult', + name='status', + field=models.CharField(choices=[('accepted', 'Accepted'), ('wrong_answer', 'Wrong Answer'), ('compile_error', 'Compile Error'), ('time_limit_exceeded', 'Time Limit Exceeded'), ('memory_limit_exceeded', 'Memory Limit Exceeded'), ('runtime_error', 'Runtime Error')], max_length=30), + ), + migrations.AlterField( + model_name='submissionresult', + name='test_case_id', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/submissions/migrations/0005_alter_submissionresult_options_and_more.py b/submissions/migrations/0005_alter_submissionresult_options_and_more.py new file mode 100644 index 0000000..5689dd5 --- /dev/null +++ b/submissions/migrations/0005_alter_submissionresult_options_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.7 on 2025-12-28 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0004_alter_submissionresult_status_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='submissionresult', + options={'ordering': ['subtask_id', 'test_case_index']}, + ), + migrations.RemoveIndex( + model_name='submissionresult', + name='submission__submiss_cb29e7_idx', + ), + migrations.AddField( + model_name='submissionresult', + name='subtask_id', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AlterField( + model_name='submissionresult', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterUniqueTogether( + name='submissionresult', + unique_together={('submission', 'subtask_id', 'test_case_index')}, + ), + migrations.AddIndex( + model_name='submissionresult', + index=models.Index(fields=['submission', 'subtask_id', 'test_case_index'], name='submission__submiss_37b35f_idx'), + ), + ] diff --git a/submissions/models.py b/submissions/models.py index 116ec23..0475068 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -108,6 +108,7 @@ class SubmissionResult(models.Model): STATUS_CHOICES = [ ('accepted', 'Accepted'), ('wrong_answer', 'Wrong Answer'), + ('compile_error', 'Compile Error'), ('time_limit_exceeded', 'Time Limit Exceeded'), ('memory_limit_exceeded', 'Memory Limit Exceeded'), ('runtime_error', 'Runtime Error'), @@ -119,14 +120,14 @@ class SubmissionResult(models.Model): ('solved', 'Solved'), ] - # Primary key - UUID - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + # Foreign keys - 複合主鍵的一部分 + submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name='results') + subtask_id = models.IntegerField() # 複合主鍵:第幾個子題 + test_case_index = models.IntegerField() # 複合主鍵:第幾筆測資(相對於 subtask) - # Foreign keys + # Other foreign keys problem_id = models.IntegerField() - submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name='results') - test_case_id = models.IntegerField() - test_case_index = models.IntegerField() + test_case_id = models.IntegerField(null=True, blank=True) # CE 時可能為 None # Status and scoring status = models.CharField(max_length=30, choices=STATUS_CHOICES) @@ -149,10 +150,12 @@ class SubmissionResult(models.Model): created_at = models.DateTimeField(auto_now_add=True) class Meta: + # 複合主鍵:submission + subtask_id + test_case_index + unique_together = [['submission', 'subtask_id', 'test_case_index']] indexes = [ - models.Index(fields=['submission', 'test_case_index']), + models.Index(fields=['submission', 'subtask_id', 'test_case_index']), ] - ordering = ['test_case_index'] + ordering = ['subtask_id', 'test_case_index'] db_table = 'submission_results' def __str__(self): diff --git a/submissions/sandbox_client.py b/submissions/sandbox_client.py index 8075445..99eeb8d 100644 --- a/submissions/sandbox_client.py +++ b/submissions/sandbox_client.py @@ -96,7 +96,7 @@ def submit_to_sandbox(submission): # 只有在啟用自訂 checker 時才使用設定的 checker_name,否則強制使用 'diff' 'use_checker': problem.use_custom_checker, 'checker_name': problem.checker_name if problem.use_custom_checker else 'diff', - 'use_static_analysis': False, # TODO: 從 assignment 設定取得 + 'use_static_analysis': problem.use_static_analysis, 'priority': 0, # 一般優先級 'callback_url': settings.BACKEND_BASE_URL.rstrip('/'), # Sandbox 判題完成後回傳結果的 URL(注意:是 submission 不是 submissions) } @@ -116,6 +116,13 @@ def submit_to_sandbox(submission): headers = {} if SANDBOX_API_KEY: headers['X-API-KEY'] = SANDBOX_API_KEY + logger.info(f'Using API Key: {SANDBOX_API_KEY[:10]}...') # 只顯示前 10 個字元 + else: + logger.warning('SANDBOX_API_KEY is not set!') + + logger.debug(f'Request URL: {url}') + logger.debug(f'Request data: {data}') + logger.debug(f'Request headers: {headers}') response = requests.post( url, @@ -125,7 +132,10 @@ def submit_to_sandbox(submission): timeout=SANDBOX_TIMEOUT ) - # 8. 檢查回應 + # 7. 檢查回應 + logger.info(f'Sandbox response status: {response.status_code}') + if response.status_code >= 400: + logger.error(f'Sandbox error response: {response.text}') response.raise_for_status() result = response.json() @@ -161,8 +171,16 @@ def submit_selftest_to_sandbox(problem_id, language_type, source_code, stdin_dat """ import uuid import hashlib + from problems.models import Problems try: + # 取得題目資訊(用於靜態分析設定) + try: + problem = Problems.objects.get(id=problem_id) + except Problems.DoesNotExist: + logger.error(f'Problem {problem_id} not found for selftest') + return None + # 產生臨時 ID(不存 DB,只用於追蹤) temp_id = f"selftest-{uuid.uuid4()}" @@ -186,7 +204,7 @@ def submit_selftest_to_sandbox(problem_id, language_type, source_code, stdin_dat 'memory_limit': 262144, # 256 MB 'use_checker': False, 'checker_name': 'diff', - 'use_static_analysis': False, + 'use_static_analysis': problem.use_static_analysis, 'priority': -1, # 低優先級(自定義測試不影響正式提交) 'callback_url': settings.BACKEND_BASE_URL.rstrip('/'), # Custom test callback URL } diff --git a/submissions/serializers.py b/submissions/serializers.py index 8dff035..8d44b1c 100644 --- a/submissions/serializers.py +++ b/submissions/serializers.py @@ -151,7 +151,7 @@ class SubmissionResultSerializer(serializers.ModelSerializer): class Meta: model = SubmissionResult fields = '__all__' - read_only_fields = ['id', 'created_at'] + read_only_fields = ['created_at'] # 移除 'id',因為不再有單一主鍵 class UserProblemStatsSerializer(serializers.ModelSerializer): diff --git a/submissions/test_file/test_callback_apis.py b/submissions/test_file/test_callback_apis.py index 6f972d5..d0b0988 100644 --- a/submissions/test_file/test_callback_apis.py +++ b/submissions/test_file/test_callback_apis.py @@ -2,9 +2,9 @@ """ 測試 Sandbox Callback APIs -測試 Backend 接收 Sandbox 判題結果的 callback endpoints: -1. POST /submission/callback/ - 正式提交結果 -2. POST /submission/custom-test-callback/ - 自定義測試結果 +測試以下兩個 API: +1. SubmissionCallbackAPIView - 正式提交的 callback +2. CustomTestCallbackAPIView - 自定義測試的 callback 使用方式: cd /Users/keliangyun/Desktop/software_engineering/back_end @@ -19,685 +19,614 @@ from datetime import datetime # 設定 Django 環境 -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'back_end.settings') django.setup() -from django.conf import settings +from django.contrib.auth import get_user_model from submissions.models import Submission, SubmissionResult, CustomTest -from user.models import User from problems.models import Problems, Problem_subtasks, Test_cases -from courses.models import Courses -import uuid - -# API 設定 -BASE_URL = "http://localhost:8000" -API_KEY = settings.SANDBOX_API_KEY # 從 settings 讀取 - - -class Colors: - """終端顏色""" - GREEN = '\033[92m' - RED = '\033[91m' - YELLOW = '\033[93m' - BLUE = '\033[94m' - CYAN = '\033[96m' - END = '\033[0m' - BOLD = '\033[1m' - - -def print_header(text): - """列印標題""" - print(f"\n{Colors.BOLD}{Colors.CYAN}{'=' * 70}{Colors.END}") - print(f"{Colors.BOLD}{Colors.CYAN}{text.center(70)}{Colors.END}") - print(f"{Colors.BOLD}{Colors.CYAN}{'=' * 70}{Colors.END}\n") - - -def print_success(text): - """列印成功訊息""" - print(f"{Colors.GREEN}✓ {text}{Colors.END}") +from django.conf import settings +User = get_user_model() -def print_error(text): - """列印錯誤訊息""" - print(f"{Colors.RED}✗ {text}{Colors.END}") +# 測試配置 +BACKEND_URL = "http://127.0.0.1:8443" +API_KEY = getattr(settings, 'SANDBOX_API_KEY', 'happylittle7') -def print_info(text): - """列印資訊""" - print(f"{Colors.BLUE}ℹ {text}{Colors.END}") +def print_section(title): + """列印分隔線""" + print("\n" + "=" * 70) + print(f" {title}") + print("=" * 70) -def print_warning(text): - """列印警告""" - print(f"{Colors.YELLOW}⚠ {text}{Colors.END}") +def print_result(success, message): + """列印測試結果""" + status = "✓ 成功" if success else "✗ 失敗" + print(f"{status}: {message}") def create_test_data(): - """建立測試資料""" - print_header("準備測試資料") + """創建測試用的資料""" + print_section("準備測試資料") + # 1. 創建測試用戶 try: - # 1. 確保有測試用戶 - user, created = User.objects.get_or_create( - username='test_callback_user', - defaults={ - 'email': 'test_callback@example.com', - 'real_name': 'Test Callback User', - 'identity': 'student' - } - ) - if created: - user.set_password('testpass123') - user.save() - print_success(f"建立測試用戶: {user.username}") - else: - print_info(f"使用現有測試用戶: {user.username}") - - # 2. 確保有課程(題目需要課程) - course, created = Courses.objects.get_or_create( - name='Test Course for Callback', - defaults={ - 'description': 'Test course for callback testing', - 'teacher_id': user, - 'is_active': True - } - ) - if created: - print_success(f"建立測試課程: {course.name}") - else: - print_info(f"使用現有測試課程: {course.name}") - - # 3. 使用現有題目或建立最簡單的題目 + user = User.objects.get(username='test_callback') + print_result(True, f"使用現有用戶: {user.username}") + except User.DoesNotExist: try: - # 嘗試找任意一個現有題目 - problem = Problems.objects.first() - if not problem: - # 如果沒有題目,建立最基本的題目(只填必要欄位) - problem = Problems.objects.create( - title='Test Problem for Callback', - difficulty='easy', - description='Test problem', - creator_id=user, - course_id=course, - is_public='public' - ) - print_success(f"建立測試題目: {problem.title}") - else: - print_info(f"使用現有題目: ID={problem.id}, {problem.title}") - except Exception as e: - print_error(f"無法存取題目資料: {str(e)}") - raise - - # 4. 確保有 subtask 和 test case - subtask = Problem_subtasks.objects.filter(problem_id=problem).first() - if not subtask: - subtask = Problem_subtasks.objects.create( - problem_id=problem, - subtask_no=1, - weight=100, - time_limit_ms=1000, - memory_limit_mb=256 + user = User.objects.create_user( + username='test_callback', + email='test_callback@example.com', + password='test123456' ) - print_success(f"建立 Subtask 1") - else: - print_info(f"使用現有 Subtask: {subtask.subtask_no}") - - test_case = Test_cases.objects.filter(subtask_id=subtask).first() - if not test_case: - test_case = Test_cases.objects.create( - subtask_id=subtask, - idx=1, - input_path='test/1.in', - output_path='test/1.out', - status='ready' - ) - print_success(f"建立 Test Case 1") - else: - print_info(f"使用現有 Test Case: {test_case.idx}") - - # 5. 建立測試 Submission - submission = Submission.objects.create( - user=user, - problem_id=problem.id, - language_type=2, # Python - source_code='print(sum(map(int, input().split())))', - status=-1, # Pending - score=0 - ) - print_success(f"建立測試 Submission: {submission.id}") - - # 6. 建立測試 CustomTest - custom_test = CustomTest.objects.create( - id=str(uuid.uuid4()), - user=user, - problem_id=problem.id, - language_type=2, # Python - source_code='print("Hello, World!")', - input_data='', - status=0 # Pending - ) - print_success(f"建立測試 CustomTest: {custom_test.id}") - - return { - 'user': user, - 'problem': problem, - 'test_case': test_case, - 'submission': submission, - 'custom_test': custom_test + print_result(True, f"創建新用戶: {user.username}") + except Exception as e: + # 如果 email 已存在,嘗試找到該用戶或使用不同的 email + try: + user = User.objects.get(email='test_callback@example.com') + print_result(True, f"使用現有用戶 (透過 email): {user.username}") + except User.DoesNotExist: + # 使用帶時間戳的 email + import time + unique_email = f'test_callback_{int(time.time())}@example.com' + user = User.objects.create_user( + username='test_callback', + email=unique_email, + password='test123456' + ) + print_result(True, f"創建新用戶 (unique email): {user.username}") + + # 2. 創建測試題目(如果不存在) + try: + problem = Problems.objects.get(id=1) + print_result(True, f"使用現有題目: Problem ID {problem.id}") + except Problems.DoesNotExist: + print_result(False, "找不到 Problem ID 1,請先創建測試題目") + return None, None, None, None + + # 3. 創建 subtask 和 test_case(如果不存在) + subtask, created = Problem_subtasks.objects.get_or_create( + problem_id=problem.id, + subtask_no=1, + defaults={ + 'time_limit_ms': 1000, + 'memory_limit_mb': 256, + 'score': 100 } - - except Exception as e: - print_error(f"建立測試資料失敗: {str(e)}") - import traceback - traceback.print_exc() - return None - - -def test_submission_callback_success(submission_id, test_case_id): - """測試 1: 正式提交 Callback - 成功案例""" - print_header("測試 1: 正式提交 Callback - 成功案例 (Accepted)") - - # 重置 submission - submission = Submission.objects.get(id=submission_id) - submission.status = -1 # Pending - submission.score = 0 - submission.save() - SubmissionResult.objects.filter(submission=submission).delete() - - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY - } - data = { - 'submission_id': str(submission_id), - 'status': 'accepted', - 'score': 100, - 'execution_time': 123, - 'memory_usage': 1024, - 'test_results': [ + ) + if created: + print_result(True, f"創建 Subtask 1") + else: + print_result(True, f"使用現有 Subtask 1") + + test_case, created = Test_cases.objects.get_or_create( + subtask_id=subtask, # 使用 subtask 實例 + idx=1, + defaults={ + 'input_path': '/media/testcases/problem_1/subtask_1/1.in', + 'output_path': '/media/testcases/problem_1/subtask_1/1.out', + 'status': 'ready' + } + ) + if created: + print_result(True, f"創建 Test Case 1") + else: + print_result(True, f"使用現有 Test Case 1 (ID: {test_case.id})") + + # 4. 創建測試提交 + submission = Submission.objects.create( + user=user, + problem_id=problem.id, + language_type=2, # Python + source_code='print(int(input()) + int(input()))', + status='-1', # Pending + score=0 + ) + print_result(True, f"創建測試提交: {submission.id}") + + return user, problem, test_case, submission + + +def test_submission_callback(submission_id, test_case_id): + """測試正式提交的 callback API""" + print_section("測試 1: Submission Callback API (AC 情況)") + + url = f"{BACKEND_URL}/submission/callback/" + + # 準備測試資料(按照文件規格) + payload = { + "submission_id": str(submission_id), + "status": "accepted", + "score": 100, + "execution_time": 123, + "memory_usage": 1024, + "test_results": [ { - 'test_case_id': test_case_id, - 'test_case_index': 1, - 'status': 'accepted', - 'execution_time': 123, - 'memory_usage': 1024, - 'score': 100, - 'max_score': 100, - 'error_message': None + "test_case_id": test_case_id, # 使用實際的資料庫 ID + "test_case_index": 1, # 顯示編號 + "status": "accepted", + "execution_time": 50, + "memory_usage": 512, + "score": 100, + "max_score": 100, + "error_message": None } ] } - print_info(f"POST {url}") - print_info(f"Submission ID: {submission_id}") + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY + } + + print(f"\n請求 URL: {url}") + print(f"API Key: {API_KEY}") + print(f"Payload: {json.dumps(payload, indent=2)}") try: - response = requests.post(url, json=data, headers=headers) - print_info(f"狀態碼: {response.status_code}") - print_info(f"回應: {response.text}") + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") if response.status_code == 200: - result = response.json() - if result.get('status') == 'ok': - print_success("測試通過 - Callback 成功處理") - - # 驗證資料庫更新 - submission = Submission.objects.get(id=submission_id) - if submission.status == '0' and submission.score == 100: # '0' = AC - print_success("資料庫驗證通過 - Submission 已更新") - else: - print_error(f"資料庫驗證失敗 - status={submission.status} (期望 '0'), score={submission.score} (期望 100)") - - # 驗證 SubmissionResult 建立 - results_count = SubmissionResult.objects.filter(submission=submission).count() - if results_count == 1: - print_success("資料庫驗證通過 - SubmissionResult 已建立") - else: - print_error(f"資料庫驗證失敗 - 期望 1 筆結果,實際 {results_count} 筆") - - return True + print_result(True, "Callback 處理成功") + + # 驗證資料庫更新 + submission = Submission.objects.get(id=submission_id) + if submission.status == '0': # AC + print_result(True, f"Submission 狀態已更新為 AC") else: - print_error(f"測試失敗 - 回應狀態錯誤: {result}") - return False + print_result(False, f"Submission 狀態錯誤: {submission.status}") + + if submission.score == 100: + print_result(True, f"Submission 分數已更新: {submission.score}") + else: + print_result(False, f"Submission 分數錯誤: {submission.score}") + + # 檢查 SubmissionResult + results = SubmissionResult.objects.filter(submission_id=submission_id) + if results.count() > 0: + print_result(True, f"已創建 {results.count()} 筆 SubmissionResult") + for result in results: + print(f" - Test Case {result.test_case_index}: {result.status}, Score: {result.score}/{result.max_score}") + else: + print_result(False, "沒有創建 SubmissionResult") + + return True else: - print_error(f"測試失敗 - HTTP {response.status_code}") + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") return False except Exception as e: - print_error(f"測試失敗 - {str(e)}") - import traceback - traceback.print_exc() + print_result(False, f"請求失敗: {str(e)}") return False -def test_submission_callback_wrong_answer(submission_id, test_case_id): - """測試 2: 正式提交 Callback - Wrong Answer""" - print_header("測試 2: 正式提交 Callback - Wrong Answer") - - # 重置 submission - submission = Submission.objects.get(id=submission_id) - submission.status = -1 - submission.score = 0 - submission.save() - SubmissionResult.objects.filter(submission=submission).delete() - - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY - } - data = { - 'submission_id': str(submission_id), - 'status': 'wrong_answer', - 'score': 50, - 'execution_time': 100, - 'memory_usage': 1024, - 'test_results': [ - { - 'test_case_id': test_case_id, - 'test_case_index': 1, - 'status': 'wrong_answer', - 'execution_time': 100, - 'memory_usage': 1024, - 'score': 50, - 'max_score': 100, - 'error_message': 'Expected: 5, Got: 4' - } - ] - } - - print_info(f"POST {url}") +def test_submission_callback_with_ce(): + """測試編譯錯誤的 callback(test_case_id 為 None)""" + print_section("測試 2: Submission Callback API (Compile Error)") + # 創建新的提交用於測試 CE try: - response = requests.post(url, json=data, headers=headers) - - if response.status_code == 200: - result = response.json() - if result.get('status') == 'ok': - print_success("測試通過 - WA 回應成功處理") - - submission = Submission.objects.get(id=submission_id) - if submission.status == '1' and submission.score == 50: # '1' = WA - print_success("資料庫驗證通過 - WA 狀態正確") - return True - else: - print_error(f"資料庫驗證失敗 - status={submission.status} (期望 '1'), score={submission.score} (期望 50)") - return False - - print_error(f"測試失敗 - HTTP {response.status_code}") + user = User.objects.filter(username='test_callback').first() or User.objects.first() + if not user: + print_result(False, "找不到任何用戶") + return False + problem = Problems.objects.get(id=1) + except Problems.DoesNotExist: + print_result(False, "找不到測試題目") return False - except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"錯誤: {str(e)}") return False - - -def test_submission_callback_no_api_key(submission_id): - """測試 3: 正式提交 Callback - 缺少 API Key""" - print_header("測試 3: 正式提交 Callback - 缺少 API Key (應失敗)") - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json' - # 故意不加 X-API-KEY + ce_submission = Submission.objects.create( + user=user, + problem_id=problem.id, + language_type=2, # Python + source_code='print(invalid syntax', # 故意的語法錯誤 + status='-1', # Pending + score=0 + ) + + url = f"{BACKEND_URL}/submission/callback/" + + # CE 的測試資料(test_case_id 為 None) + payload = { + "submission_id": str(ce_submission.id), + "status": "compile_error", + "score": 0, + "execution_time": 0, + "memory_usage": 0, + "test_results": [ + { + "test_case_id": None, # CE 時沒有 test_case_id + "test_case_index": 1, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 100, + "error_message": "SyntaxError: invalid syntax at line 1" + }, + { + "test_case_id": None, + "test_case_index": 2, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 100, + "error_message": "SyntaxError: invalid syntax at line 1" + } + ] } - data = { - 'submission_id': str(submission_id), - 'status': 'accepted', - 'score': 100, - 'execution_time': 123, - 'memory_usage': 1024, - 'test_results': [] + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY } - print_info(f"POST {url}") - print_warning("故意不傳送 API Key") + print(f"\n請求 URL: {url}") + print(f"Submission ID: {ce_submission.id}") + print(f"測試多筆 CE 測資...") try: - response = requests.post(url, json=data, headers=headers) + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") - if response.status_code == 401: - print_success("測試通過 - 正確拒絕未授權請求 (401)") + if response.status_code == 200: + print_result(True, "CE Callback 處理成功") + + # 驗證資料庫更新 + submission = Submission.objects.get(id=ce_submission.id) + if submission.status == '2': # CE + print_result(True, f"Submission 狀態已更新為 CE") + else: + print_result(False, f"Submission 狀態錯誤: {submission.status}") + + # 檢查 SubmissionResult(CE 應該也要有記錄) + results = SubmissionResult.objects.filter(submission_id=ce_submission.id) + if results.count() == 2: + print_result(True, f"已創建 {results.count()} 筆 CE SubmissionResult(多筆測資)") + for result in results: + print(f" - Test Case {result.test_case_index}: {result.status}") + if result.error_message: + print(f" Error: {result.error_message}") + else: + print_result(False, f"SubmissionResult 數量錯誤: {results.count()} (預期 2)") + return True else: - print_error(f"測試失敗 - 期望 401,實際 {response.status_code}") + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") return False except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"請求失敗: {str(e)}") return False -def test_submission_callback_wrong_api_key(submission_id): - """測試 4: 正式提交 Callback - 錯誤的 API Key""" - print_header("測試 4: 正式提交 Callback - 錯誤的 API Key (應失敗)") - - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': 'wrong-api-key-123456' - } - data = { - 'submission_id': str(submission_id), - 'status': 'accepted', - 'score': 100, - 'execution_time': 123, - 'memory_usage': 1024, - 'test_results': [] - } - - print_info(f"POST {url}") - print_warning("傳送錯誤的 API Key") +def test_custom_test_callback(): + """測試自定義測試的 callback API""" + print_section("測試 3: Custom Test Callback API") + # 創建測試用的 CustomTest(如果 model 存在) try: - response = requests.post(url, json=data, headers=headers) - - if response.status_code == 401: - print_success("測試通過 - 正確拒絕錯誤 API Key (401)") - return True - else: - print_error(f"測試失敗 - 期望 401,實際 {response.status_code}") + user = User.objects.filter(username='test_callback').first() or User.objects.first() + if not user: + print_result(False, "找不到任何用戶") return False - + problem = Problems.objects.get(id=1) + + custom_test = CustomTest.objects.create( + user=user, + problem_id=problem.id, + language_type=2, # Python + source_code='print(input())', + stdin='Hello World', + status='pending' + ) + + test_id = custom_test.id + print_result(True, f"創建測試用 CustomTest: {test_id}") + except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"無法創建 CustomTest: {str(e)}") + print("跳過 Custom Test Callback 測試") return False - - -def test_submission_callback_not_found(): - """測試 5: 正式提交 Callback - Submission 不存在""" - print_header("測試 5: 正式提交 Callback - Submission 不存在 (應失敗)") - fake_id = str(uuid.uuid4()) - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY + url = f"{BACKEND_URL}/submission/custom-test-callback/" + + # 準備測試資料 + payload = { + "submission_id": str(test_id), + "status": "completed", + "stdout": "Hello World\n", + "stderr": "", + "execution_time": 50, + "memory_usage": 512, + "exit_code": 0 } - data = { - 'submission_id': fake_id, - 'status': 'accepted', - 'score': 100, - 'execution_time': 123, - 'memory_usage': 1024, - 'test_results': [] + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY } - print_info(f"POST {url}") - print_warning(f"使用不存在的 Submission ID: {fake_id}") + print(f"\n請求 URL: {url}") + print(f"Payload: {json.dumps(payload, indent=2)}") try: - response = requests.post(url, json=data, headers=headers) + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") - if response.status_code == 404: - print_success("測試通過 - 正確回傳 404 Not Found") + if response.status_code == 200: + print_result(True, "Custom Test Callback 處理成功") + + # 驗證資料庫更新 + custom_test.refresh_from_db() + if custom_test.status == 'completed': + print_result(True, f"CustomTest 狀態已更新為 completed") + else: + print_result(False, f"CustomTest 狀態錯誤: {custom_test.status}") + + if custom_test.actual_output == "Hello World\n": + print_result(True, f"CustomTest 輸出已保存") + else: + print_result(False, f"CustomTest 輸出錯誤: {custom_test.actual_output}") + return True else: - print_error(f"測試失敗 - 期望 404,實際 {response.status_code}") + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") return False except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"請求失敗: {str(e)}") return False -def test_submission_callback_missing_field(): - """測試 6: 正式提交 Callback - 缺少必要欄位""" - print_header("測試 6: 正式提交 Callback - 缺少 submission_id (應失敗)") +def test_api_key_authentication(): + """測試 API Key 認證""" + print_section("測試 4: API Key 認證") - url = f"{BASE_URL}/submission/callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY - } - data = { - # 故意不傳 submission_id - 'status': 'accepted', - 'score': 100, - 'execution_time': 123, - 'memory_usage': 1024, - 'test_results': [] + try: + user = User.objects.filter(username='test_callback').first() or User.objects.first() + if not user: + print_result(False, "找不到任何用戶") + return False + problem = Problems.objects.get(id=1) + except Problems.DoesNotExist: + print_result(False, "找不到測試題目") + return False + except Exception as e: + print_result(False, f"錯誤: {str(e)}") + return False + + submission = Submission.objects.create( + user=user, + problem_id=problem.id, + language_type=2, + source_code='print("test")', + status='-1', + score=0 + ) + + url = f"{BACKEND_URL}/submission/callback/" + + payload = { + "submission_id": str(submission.id), + "status": "accepted", + "score": 100, + "execution_time": 100, + "memory_usage": 1000, + "test_results": [] } - print_info(f"POST {url}") - print_warning("故意不傳送 submission_id") + # 測試錯誤的 API Key + headers = { + "Content-Type": "application/json", + "X-API-KEY": "wrong_api_key" + } + print("\n測試錯誤的 API Key...") try: - response = requests.post(url, json=data, headers=headers) + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"狀態碼: {response.status_code}") - if response.status_code == 400: - print_success("測試通過 - 正確回傳 400 Bad Request") + if response.status_code == 401: + print_result(True, "正確拒絕了錯誤的 API Key (401 Unauthorized)") return True else: - print_error(f"測試失敗 - 期望 400,實際 {response.status_code}") + print_result(False, f"應該回傳 401,但回傳了 {response.status_code}") return False - except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"請求失敗: {str(e)}") return False -def test_custom_test_callback_success(custom_test_id): - """測試 7: 自定義測試 Callback - 成功案例""" - print_header("測試 7: 自定義測試 Callback - 成功案例") - - # 重置 custom test - custom_test = CustomTest.objects.get(id=custom_test_id) - custom_test.status = 0 # Pending - custom_test.actual_output = None - custom_test.error_message = None - custom_test.save() - - url = f"{BASE_URL}/submission/custom-test-callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY - } - data = { - 'submission_id': str(custom_test_id), - 'status': 'completed', - 'stdout': 'Hello, World!\n', - 'stderr': '', - 'execution_time': 50, - 'memory_usage': 512, - 'exit_code': 0 - } - - print_info(f"POST {url}") - print_info(f"CustomTest ID: {custom_test_id}") +def test_multiple_test_cases(): + """測試多筆測資的情況""" + print_section("測試 5: 多筆測資 Callback") try: - response = requests.post(url, json=data, headers=headers) - print_info(f"狀態碼: {response.status_code}") - print_info(f"回應: {response.text}") - - if response.status_code == 200: - result = response.json() - if result.get('status') == 'ok': - print_success("測試通過 - Custom Test Callback 成功處理") - - # 驗證資料庫更新 - custom_test = CustomTest.objects.get(id=custom_test_id) - if custom_test.status == 'completed' and custom_test.actual_output == 'Hello, World!\n': - print_success("資料庫驗證通過 - CustomTest 已更新") - return True - else: - print_error(f"資料庫驗證失敗 - status={custom_test.status} (期望 'completed'), actual_output={repr(custom_test.actual_output)}") - return False - - print_error(f"測試失敗 - HTTP {response.status_code}") + user = User.objects.filter(username='test_callback').first() or User.objects.first() + if not user: + print_result(False, "找不到任何用戶") + return False + problem = Problems.objects.get(id=1) + except Problems.DoesNotExist: + print_result(False, "找不到測試題目") return False - except Exception as e: - print_error(f"測試失敗 - {str(e)}") - import traceback - traceback.print_exc() + print_result(False, f"錯誤: {str(e)}") return False - - -def test_custom_test_callback_error(custom_test_id): - """測試 8: 自定義測試 Callback - 錯誤案例""" - print_header("測試 8: 自定義測試 Callback - 執行錯誤") - - # 重置 custom test - custom_test = CustomTest.objects.get(id=custom_test_id) - custom_test.status = 0 # Pending - custom_test.actual_output = None - custom_test.error_message = None - custom_test.save() - - url = f"{BASE_URL}/submission/custom-test-callback/" - headers = { - 'Content-Type': 'application/json', - 'X-API-KEY': API_KEY - } - data = { - 'submission_id': str(custom_test_id), - 'status': 'error', - 'stdout': '', - 'stderr': 'ZeroDivisionError: division by zero', - 'execution_time': 10, - 'memory_usage': 512, - 'exit_code': 1 - } - print_info(f"POST {url}") + # 獲取或創建多個 test case + subtask = Problem_subtasks.objects.filter(problem_id=problem.id).first() - try: - response = requests.post(url, json=data, headers=headers) - - if response.status_code == 200: - result = response.json() - if result.get('status') == 'ok': - print_success("測試通過 - Error 回應成功處理") - - custom_test = CustomTest.objects.get(id=custom_test_id) - if custom_test.status == 'error' and custom_test.error_message: # 'error' 狀態 - print_success("資料庫驗證通過 - Error 狀態正確") - return True - else: - print_error(f"資料庫驗證失敗 - status={custom_test.status} (期望 'error'), error_message={custom_test.error_message}") - return False - - print_error(f"測試失敗 - HTTP {response.status_code}") - return False - - except Exception as e: - print_error(f"測試失敗 - {str(e)}") - return False - - -def test_custom_test_callback_no_auth(custom_test_id): - """測試 9: 自定義測試 Callback - 缺少認證""" - print_header("測試 9: 自定義測試 Callback - 缺少認證 (應失敗)") + test_cases = [] + for i in range(1, 4): # 創建 3 個 test case + tc, created = Test_cases.objects.get_or_create( + subtask_id=subtask, # 使用 subtask 實例 + idx=i, + defaults={ + 'input_path': f'/media/testcases/problem_1/subtask_1/{i}.in', + 'output_path': f'/media/testcases/problem_1/subtask_1/{i}.out', + 'status': 'ready' + } + ) + test_cases.append(tc) + + # 創建新提交 + submission = Submission.objects.create( + user=user, + problem_id=problem.id, + language_type=2, + source_code='print("test")', + status='-1', + score=0 + ) + + url = f"{BACKEND_URL}/submission/callback/" + + # 準備多筆測資的結果 + payload = { + "submission_id": str(submission.id), + "status": "wrong_answer", # 部分錯誤 + "score": 66, + "execution_time": 200, + "memory_usage": 2048, + "test_results": [ + { + "test_case_id": test_cases[0].id, + "test_case_index": 1, + "status": "accepted", + "execution_time": 60, + "memory_usage": 600, + "score": 33, + "max_score": 33, + "error_message": None + }, + { + "test_case_id": test_cases[1].id, + "test_case_index": 2, + "status": "accepted", + "execution_time": 70, + "memory_usage": 700, + "score": 33, + "max_score": 33, + "error_message": None + }, + { + "test_case_id": test_cases[2].id, + "test_case_index": 3, + "status": "wrong_answer", + "execution_time": 70, + "memory_usage": 748, + "score": 0, + "max_score": 34, + "error_message": "Expected: 7, Got: 6" + } + ] + } - url = f"{BASE_URL}/submission/custom-test-callback/" headers = { - 'Content-Type': 'application/json' - # 故意不加 X-API-KEY - } - data = { - 'submission_id': str(custom_test_id), - 'status': 'completed', - 'stdout': 'test', - 'stderr': '', - 'execution_time': 50, - 'memory_usage': 512, - 'exit_code': 0 + "Content-Type": "application/json", + "X-API-KEY": API_KEY } - print_info(f"POST {url}") - print_warning("故意不傳送 API Key") + print(f"\n請求 URL: {url}") + print(f"測試 3 筆測資(2 AC, 1 WA)...") try: - response = requests.post(url, json=data, headers=headers) + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") - if response.status_code == 401: - print_success("測試通過 - 正確拒絕未授權請求 (401)") + if response.status_code == 200: + print_result(True, "多筆測資 Callback 處理成功") + + # 驗證資料庫 + submission.refresh_from_db() + results = SubmissionResult.objects.filter(submission_id=submission.id).order_by('test_case_index') + + if results.count() == 3: + print_result(True, f"已創建 3 筆 SubmissionResult") + for result in results: + print(f" - Test Case {result.test_case_index}: {result.status}, Score: {result.score}/{result.max_score}") + if result.error_message: + print(f" Error: {result.error_message}") + else: + print_result(False, f"SubmissionResult 數量錯誤: {results.count()} (預期 3)") + return True else: - print_error(f"測試失敗 - 期望 401,實際 {response.status_code}") + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") return False except Exception as e: - print_error(f"測試失敗 - {str(e)}") + print_result(False, f"請求失敗: {str(e)}") return False def main(): - """主測試流程""" - print(f""" -╔══════════════════════════════════════════════════════════════════╗ -║ Sandbox Callback APIs 測試工具 ║ -║ 測試 Backend 接收 Sandbox 判題結果的功能 ║ -╚══════════════════════════════════════════════════════════════════╝ - """) - - # 檢查 Django Server - print_header("前置檢查") + """主函數""" + print("\n╔" + "=" * 68 + "╗") + print("║" + " " * 15 + "Sandbox Callback APIs 測試腳本" + " " * 24 + "║") + print("╚" + "=" * 68 + "╝") + + # 確認 Django 伺服器正在運行 + print_section("檢查 Django 伺服器") try: - # 改用 admin 頁面檢查,因為 /api/ 可能不存在 - response = requests.get(f"{BASE_URL}/admin/", timeout=5, allow_redirects=False) - if response.status_code in [200, 302]: # 200 或重定向都表示 server 運行中 - print_success("Django Server 運行中") - else: - print_error(f"Django Server 異常 - HTTP {response.status_code}") - print_warning("請確認 Django Server 是否正常運行") - return - except requests.RequestException as e: - print_error(f"無法連接到 Django Server: {str(e)}") - print_warning("請先啟動 Django Server: python manage.py runserver") + response = requests.get(f"{BACKEND_URL}/", timeout=5) + print_result(True, f"Django 伺服器運行中 ({BACKEND_URL})") + except Exception as e: + print_result(False, f"無法連接到 Django 伺服器: {str(e)}") + print("\n請確保 Django 伺服器正在運行:") + print(" python manage.py runserver 0.0.0.0:8443") return - # 建立測試資料 - test_data = create_test_data() - if not test_data: - print_error("無法建立測試資料,終止測試") + # 創建測試資料 + user, problem, test_case, submission = create_test_data() + if not submission: + print("\n測試終止:無法創建必要的測試資料") return - submission_id = test_data['submission'].id - custom_test_id = test_data['custom_test'].id - test_case_id = test_data['test_case'].id - # 執行測試 results = [] - # 正式提交 Callback 測試 - results.append(("正式提交 - 成功案例", test_submission_callback_success(submission_id, test_case_id))) - results.append(("正式提交 - Wrong Answer", test_submission_callback_wrong_answer(submission_id, test_case_id))) - results.append(("正式提交 - 缺少 API Key", test_submission_callback_no_api_key(submission_id))) - results.append(("正式提交 - 錯誤 API Key", test_submission_callback_wrong_api_key(submission_id))) - results.append(("正式提交 - Submission 不存在", test_submission_callback_not_found())) - results.append(("正式提交 - 缺少必要欄位", test_submission_callback_missing_field())) + # Test 1: 正常的提交 callback (AC) + results.append(test_submission_callback(submission.id, test_case.id)) - # 自定義測試 Callback 測試 - results.append(("自定義測試 - 成功案例", test_custom_test_callback_success(custom_test_id))) - results.append(("自定義測試 - 執行錯誤", test_custom_test_callback_error(custom_test_id))) - results.append(("自定義測試 - 缺少認證", test_custom_test_callback_no_auth(custom_test_id))) + # Test 2: CE 的提交 callback(多筆測資) + results.append(test_submission_callback_with_ce()) - # 測試總結 - print_header("測試總結") + # Test 3: 自定義測試 callback + results.append(test_custom_test_callback()) - passed = sum(1 for _, result in results if result) - total = len(results) + # Test 4: API Key 認證 + results.append(test_api_key_authentication()) - for test_name, result in results: - if result: - print_success(f"{test_name}") - else: - print_error(f"{test_name}") + # Test 5: 多筆測資 + results.append(test_multiple_test_cases()) - print(f"\n{Colors.BOLD}總計: {passed}/{total} 測試通過{Colors.END}") + # 總結 + print_section("測試總結") + passed = sum(results) + total = len(results) + print(f"通過: {passed}/{total}") if passed == total: - print(f"\n{Colors.GREEN}{Colors.BOLD}🎉 所有測試通過!{Colors.END}") + print("\n🎉 所有測試通過!") else: - print(f"\n{Colors.RED}{Colors.BOLD}⚠️ 有 {total - passed} 個測試失敗{Colors.END}") - - print(f"\n{Colors.CYAN}{'=' * 70}{Colors.END}\n") + print(f"\n⚠️ 有 {total - passed} 個測試失敗") -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/submissions/test_file/test_ce_callback.py b/submissions/test_file/test_ce_callback.py new file mode 100644 index 0000000..369785f --- /dev/null +++ b/submissions/test_file/test_ce_callback.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python +""" +測試 Compile Error (CE) Callback + +專門測試 Sandbox 回傳 CE 的情況,包括: +1. 單筆測資 CE +2. 多筆測資 CE +3. test_case_id 為 None 的情況 +4. error_message 的儲存 + +使用方式: + cd /Users/keliangyun/Desktop/software_engineering/back_end + python submissions/test_file/test_ce_callback.py +""" + +import os +import sys +import django +import requests +import json + +# 設定 Django 環境 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'back_end.settings') +django.setup() + +from django.contrib.auth import get_user_model +from submissions.models import Submission, SubmissionResult +from problems.models import Problems +from django.conf import settings + +User = get_user_model() + +# 測試配置 +BACKEND_URL = "http://127.0.0.1:8443" +API_KEY = getattr(settings, 'SANDBOX_API_KEY', 'happylittle7') + + +def print_section(title): + """列印分隔線""" + print("\n" + "=" * 70) + print(f" {title}") + print("=" * 70) + + +def print_result(success, message): + """列印測試結果""" + status = "✓ 成功" if success else "✗ 失敗" + print(f"{status}: {message}") + + +def create_ce_submission(): + """創建一個用於測試 CE 的提交""" + try: + user = User.objects.first() + if not user: + print_result(False, "找不到任何用戶") + return None + + problem = Problems.objects.first() + if not problem: + print_result(False, "找不到任何題目") + return None + + submission = Submission.objects.create( + user=user, + problem_id=problem.id, + language_type=2, # Python + source_code='print(invalid syntax # 故意的語法錯誤', + status='-1', # Pending + score=0 + ) + + print_result(True, f"創建測試提交: {submission.id}") + return submission + + except Exception as e: + print_result(False, f"創建提交失敗: {str(e)}") + return None + + +def test_single_ce(): + """測試單筆測資的 CE""" + print_section("測試 1: 單筆測資 CE") + + submission = create_ce_submission() + if not submission: + return False + + url = f"{BACKEND_URL}/submission/callback/" + + # 單筆 CE 的 payload + payload = { + "submission_id": str(submission.id), + "status": "compile_error", + "score": 0, + "execution_time": 0, + "memory_usage": 0, + "test_results": [ + { + "test_case_id": None, # CE 時沒有 test_case_id + "test_case_index": 1, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 100, + "error_message": " File \"\", line 1\n print(invalid syntax\n ^\nSyntaxError: invalid syntax" + } + ] + } + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY + } + + print(f"\n請求 URL: {url}") + print(f"Submission ID: {submission.id}") + print(f"Payload:\n{json.dumps(payload, indent=2, ensure_ascii=False)}") + + try: + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") + + if response.status_code == 200: + print_result(True, "單筆 CE Callback 處理成功") + + # 驗證資料庫 + submission.refresh_from_db() + + # 檢查 Submission 狀態 + if submission.status == '2': # CE + print_result(True, f"Submission 狀態: CE ('{submission.status}')") + else: + print_result(False, f"Submission 狀態錯誤: '{submission.status}' (預期 '2')") + + if submission.score == 0: + print_result(True, f"Submission 分數: {submission.score}") + else: + print_result(False, f"Submission 分數錯誤: {submission.score}") + + # 檢查 SubmissionResult + results = SubmissionResult.objects.filter(submission_id=submission.id) + if results.count() == 1: + print_result(True, f"已創建 1 筆 SubmissionResult") + result = results.first() + print(f" - Test Case Index: {result.test_case_index}") + print(f" - Test Case ID: {result.test_case_id}") + print(f" - Status: {result.status}") + print(f" - Score: {result.score}/{result.max_score}") + if result.error_message: + print(f" - Error Message: {result.error_message[:100]}...") + print_result(True, "Error message 已儲存") + else: + print_result(False, "Error message 為空") + + # 檢查 test_case_id 是否為 None + if result.test_case_id is None: + print_result(True, "test_case_id 正確為 None") + else: + print_result(False, f"test_case_id 應為 None,但為 {result.test_case_id}") + else: + print_result(False, f"SubmissionResult 數量錯誤: {results.count()} (預期 1)") + + return True + else: + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") + return False + + except Exception as e: + print_result(False, f"請求失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def test_multiple_ce(): + """測試多筆測資的 CE(每筆都是 CE)""" + print_section("測試 2: 多筆測資 CE") + + submission = create_ce_submission() + if not submission: + return False + + url = f"{BACKEND_URL}/submission/callback/" + + # 多筆 CE 的 payload(模擬 5 筆測資都 CE) + test_results = [] + for i in range(1, 6): + test_results.append({ + "test_case_id": None, + "test_case_index": i, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 20, + "error_message": f" File \"\", line 1\n print(invalid syntax\n ^\nSyntaxError: invalid syntax (Test Case {i})" + }) + + payload = { + "submission_id": str(submission.id), + "status": "compile_error", + "score": 0, + "execution_time": 0, + "memory_usage": 0, + "test_results": test_results + } + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY + } + + print(f"\n請求 URL: {url}") + print(f"Submission ID: {submission.id}") + print(f"測試 5 筆測資,每筆都是 CE...") + + try: + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + print(f"響應: {response.text}") + + if response.status_code == 200: + print_result(True, "多筆 CE Callback 處理成功") + + # 驗證資料庫 + submission.refresh_from_db() + + if submission.status == '2': + print_result(True, f"Submission 狀態: CE") + else: + print_result(False, f"Submission 狀態錯誤: {submission.status}") + + # 檢查 SubmissionResult + results = SubmissionResult.objects.filter(submission_id=submission.id).order_by('test_case_index') + if results.count() == 5: + print_result(True, f"已創建 5 筆 SubmissionResult") + all_ce = all(r.status == 'compile_error' for r in results) + if all_ce: + print_result(True, "所有測資狀態都是 compile_error") + else: + print_result(False, "部分測資狀態不是 compile_error") + + all_have_error = all(r.error_message for r in results) + if all_have_error: + print_result(True, "所有測資都有 error_message") + else: + print_result(False, "部分測資沒有 error_message") + + all_none_test_case = all(r.test_case_id is None for r in results) + if all_none_test_case: + print_result(True, "所有測資的 test_case_id 都是 None") + else: + print_result(False, "部分測資的 test_case_id 不是 None") + + print("\n詳細資訊:") + for result in results: + print(f" - Test Case {result.test_case_index}: {result.status}") + print(f" Error: {result.error_message[:60]}...") + else: + print_result(False, f"SubmissionResult 數量錯誤: {results.count()} (預期 5)") + + return True + else: + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") + return False + + except Exception as e: + print_result(False, f"請求失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def test_ce_with_long_error_message(): + """測試帶有長 error message 的 CE""" + print_section("測試 3: 長 Error Message 的 CE") + + submission = create_ce_submission() + if not submission: + return False + + url = f"{BACKEND_URL}/submission/callback/" + + # 模擬一個很長的錯誤訊息 + long_error = """Traceback (most recent call last): + File "", line 1, in + File "/usr/lib/python3.11/some_module.py", line 123, in some_function + raise SyntaxError("invalid syntax") +SyntaxError: invalid syntax + +Additional context: +This is a very long error message that contains multiple lines +and detailed information about what went wrong during compilation. +It might include stack traces, line numbers, and other debugging +information that would be useful for the user to understand +what caused the compilation error. + +Error occurred at line 1, column 15 +Expected: expression +Found: invalid token +""" * 5 # 重複 5 次讓它更長 + + payload = { + "submission_id": str(submission.id), + "status": "compile_error", + "score": 0, + "execution_time": 0, + "memory_usage": 0, + "test_results": [ + { + "test_case_id": None, + "test_case_index": 1, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 100, + "error_message": long_error + } + ] + } + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY + } + + print(f"\n請求 URL: {url}") + print(f"Submission ID: {submission.id}") + print(f"Error Message 長度: {len(long_error)} 字元") + + try: + response = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"\n狀態碼: {response.status_code}") + + if response.status_code == 200: + print_result(True, "長 Error Message Callback 處理成功") + + # 驗證資料庫 + results = SubmissionResult.objects.filter(submission_id=submission.id) + if results.count() == 1: + result = results.first() + if result.error_message: + stored_length = len(result.error_message) + print_result(True, f"Error message 已儲存 ({stored_length} 字元)") + print(f"原始長度: {len(long_error)} 字元") + print(f"儲存長度: {stored_length} 字元") + + # 檢查是否完整儲存 + if stored_length == len(long_error): + print_result(True, "Error message 完整儲存") + else: + print_result(False, f"Error message 可能被截斷") + else: + print_result(False, "Error message 為空") + + return True + else: + print_result(False, f"HTTP 狀態碼錯誤: {response.status_code}") + return False + + except Exception as e: + print_result(False, f"請求失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def test_duplicate_ce_callback(): + """測試重複呼叫 CE callback(應該 update 而不是 create)""" + print_section("測試 4: 重複 CE Callback (update_or_create 測試)") + + submission = create_ce_submission() + if not submission: + return False + + url = f"{BACKEND_URL}/submission/callback/" + + payload = { + "submission_id": str(submission.id), + "status": "compile_error", + "score": 0, + "execution_time": 0, + "memory_usage": 0, + "test_results": [ + { + "test_case_id": None, + "test_case_index": 1, + "status": "compile_error", + "execution_time": 0, + "memory_usage": 0, + "score": 0, + "max_score": 100, + "error_message": "First error message" + } + ] + } + + headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY + } + + print(f"\n請求 URL: {url}") + print(f"Submission ID: {submission.id}") + + try: + # 第一次呼叫 + print("\n第一次呼叫 callback...") + response1 = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"狀態碼: {response1.status_code}") + + if response1.status_code != 200: + print_result(False, "第一次呼叫失敗") + return False + + # 檢查第一次的結果 + results1 = SubmissionResult.objects.filter(submission_id=submission.id) + count1 = results1.count() + print_result(True, f"第一次呼叫後有 {count1} 筆 SubmissionResult") + + # 修改 error message 並第二次呼叫 + payload["test_results"][0]["error_message"] = "Updated error message (second call)" + + print("\n第二次呼叫 callback(應該更新而不是新增)...") + response2 = requests.post(url, json=payload, headers=headers, timeout=10) + print(f"狀態碼: {response2.status_code}") + + if response2.status_code != 200: + print_result(False, "第二次呼叫失敗") + return False + + # 檢查第二次的結果 + results2 = SubmissionResult.objects.filter(submission_id=submission.id) + count2 = results2.count() + + if count2 == count1: + print_result(True, f"第二次呼叫後仍然只有 {count2} 筆 SubmissionResult(沒有重複)") + + # 檢查 error message 是否被更新 + result = results2.first() + if "Updated error message" in result.error_message: + print_result(True, "Error message 已更新") + print(f" 更新後的 message: {result.error_message}") + else: + print_result(False, "Error message 沒有更新") + print(f" 實際 message: {result.error_message}") + else: + print_result(False, f"產生了重複記錄!第一次: {count1},第二次: {count2}") + + return True + + except Exception as e: + print_result(False, f"請求失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def main(): + """主函數""" + print("\n╔" + "=" * 68 + "╗") + print("║" + " " * 15 + "Compile Error (CE) 測試腳本" + " " * 26 + "║") + print("╚" + "=" * 68 + "╝") + + # 確認 Django 伺服器正在運行 + print_section("檢查 Django 伺服器") + try: + response = requests.get(f"{BACKEND_URL}/", timeout=5) + print_result(True, f"Django 伺服器運行中 ({BACKEND_URL})") + except Exception as e: + print_result(False, f"無法連接到 Django 伺服器: {str(e)}") + print("\n請確保 Django 伺服器正在運行:") + print(" python manage.py runserver 0.0.0.0:8443") + return + + # 執行測試 + results = [] + + # Test 1: 單筆 CE + results.append(test_single_ce()) + + # Test 2: 多筆 CE + results.append(test_multiple_ce()) + + # Test 3: 長 error message + results.append(test_ce_with_long_error_message()) + + # Test 4: 重複 callback + results.append(test_duplicate_ce_callback()) + + # 總結 + print_section("測試總結") + passed = sum(results) + total = len(results) + print(f"通過: {passed}/{total}") + + if passed == total: + print("\n🎉 所有 CE 測試通過!") + print("\n重點驗證項目:") + print(" ✓ CE 狀態正確儲存") + print(" ✓ test_case_id 可以是 None") + print(" ✓ error_message 正確儲存") + print(" ✓ 多筆測資 CE 都能處理") + print(" ✓ update_or_create 避免重複記錄") + else: + print(f"\n⚠️ 有 {total - passed} 個測試失敗") + + +if __name__ == '__main__': + main() diff --git a/submissions/test_file/test_sandbox_integration.py b/submissions/test_file/test_sandbox_integration.py index 885f296..4835d02 100644 --- a/submissions/test_file/test_sandbox_integration.py +++ b/submissions/test_file/test_sandbox_integration.py @@ -13,7 +13,7 @@ import json # 測試配置 -BASE_URL = "http://127.0.0.1:8000" +BASE_URL = "http://127.0.0.1:8443" # Django 後端運行在 8443 端口 SANDBOX_URL = "http://34.81.90.111:8000" def print_section(title): diff --git a/submissions/views.py b/submissions/views.py index 0c9d7f0..e48aba2 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1090,22 +1090,58 @@ def submission_output_view(request, id, task_no, case_no): return api_response(None, "task_no not found", 404) # 4. 找 test_case(task_no + case_no) + # 注意:我們嘗試找 test_case 來驗證,但實際查詢 SubmissionResult 時主要用 test_case_index + test_case = None + test_case_id = None try: test_case = Test_cases.objects.get( subtask_id=subtask.id, idx=case_no ) + test_case_id = test_case.id except Test_cases.DoesNotExist: - return api_response(None, "case_no not found", 404) + logger.warning(f'Test case not found: subtask_id={subtask.id}, idx={case_no}') + # 即使找不到 test_case,也繼續用 test_case_index 查詢 # 5. 找結果 + # 重要:查詢需要 subtask_id + test_case_index(因為 test_case_index 只是相對於 subtask 的編號) try: - result = SubmissionResult.objects.get( + result = SubmissionResult.objects.filter( submission_id=submission.id, - test_case_id=test_case.id + subtask_id=subtask.id, + test_case_index=case_no + ).order_by('-created_at').first() + + if not result: + # 提供詳細的診斷資訊 + all_results = SubmissionResult.objects.filter(submission_id=submission.id) + total_results = all_results.count() + + # 列出該 submission 所有現有的測資結果 + existing_cases = [] + for r in all_results[:10]: # 最多顯示 10 筆 + existing_cases.append(f"subtask_id={r.subtask_id}, case_index={r.test_case_index}") + + error_detail = ( + f"找不到測資結果。" + f"查詢條件: submission_id={submission.id}, subtask_id={subtask.id}, case_no={case_no}. " + f"該 submission 共有 {total_results} 筆測資結果" + f"{': ' + ', '.join(existing_cases) if existing_cases else '(無任何測資結果)'}. " + f"可能原因: 1) 判題尚未完成 2) subtask_id 不匹配 3) case_no 超出範圍" + ) + + return api_response( + None, + error_detail, + 404 + ) + except Exception as e: + logger.error(f'Error fetching submission result: {str(e)}') + return api_response( + None, + f"查詢測資結果時發生錯誤:{str(e)}", + 404 ) - except SubmissionResult.DoesNotExist: - return api_response(None, "output not found", 404) # 6. 回傳 payload = { @@ -1888,7 +1924,8 @@ def post(self, request): memory_usage = data.get('memory_usage', 0) test_results = data.get('test_results', []) - logger.info(f'Received callback for submission {submission_id}: status={judge_status}, score={total_score}') + logger.info(f'Received callback for submission {submission_id}: status={judge_status}, score={total_score}, test_results count={len(test_results)}') + logger.debug(f'Full callback payload: {data}') if not submission_id: return api_response( @@ -1926,24 +1963,142 @@ def post(self, request): logger.info(f'Updated submission {submission_id}: status={submission.status}, score={total_score}') # 4. 建立 SubmissionResult 記錄 - for test_result in test_results: - # 轉換 status 為字串格式(SubmissionResult 使用字串) - result_status = test_result.get('status', 'runtime_error') - - SubmissionResult.objects.create( - submission=submission, # 使用 submission 而非 submission_id - problem_id=submission.problem_id, - test_case_id=test_result.get('test_case_id'), - test_case_index=test_result.get('test_case_index', 0), - status=result_status, # SubmissionResult 的 status 是字串類型 - execution_time=test_result.get('execution_time'), - memory_usage=test_result.get('memory_usage'), - score=test_result.get('score', 0), - max_score=test_result.get('max_score', 100), - error_message=test_result.get('error_message'), - ) + from problems.models import Problem_subtasks, Test_cases - logger.info(f'Created {len(test_results)} test results for submission {submission_id}') + # 特殊處理:如果是 CE/SE 且 test_results 數量不足,為所有測資創建相同的錯誤記錄 + if judge_status in ['compile_error', 'system_error']: + # 取得該題目的所有測資 + all_test_cases = [] + subtasks = Problem_subtasks.objects.filter(problem_id=submission.problem_id).order_by('subtask_no') + for subtask in subtasks: + test_cases = Test_cases.objects.filter(subtask_id=subtask.id).order_by('idx') + all_test_cases.extend(test_cases) + + # 取得錯誤訊息(從第一筆 test_result 或使用預設值) + error_message = data.get('error_message', f'{judge_status.replace("_", " ").title()}') + if test_results and len(test_results) > 0: + error_message = test_results[0].get('error_message', error_message) + + # 為每個測資創建或更新錯誤記錄 + for test_case in all_test_cases: + SubmissionResult.objects.update_or_create( + submission=submission, + subtask_id=test_case.subtask_id.id, + test_case_index=test_case.idx, + defaults={ + 'problem_id': submission.problem_id, + 'test_case_id': None, # CE/SE 時不關聯具體測資 + 'status': judge_status, + 'execution_time': 0, + 'memory_usage': 0, + 'score': 0, + 'max_score': 100, + 'error_message': error_message, + } + ) + + logger.info(f'Created {len(all_test_cases)} {judge_status} results for all test cases of submission {submission_id}') + elif judge_status == 'accepted' and len(test_results) == 0: + # 特殊處理:AC 但沒有回傳 test_results,為所有測資創建 AC 記錄 + logger.info(f'Processing test_result: specail AC with no test_results') + all_test_cases = [] + subtasks = Problem_subtasks.objects.filter(problem_id=submission.problem_id).order_by('subtask_no') + for subtask in subtasks: + test_cases = Test_cases.objects.filter(subtask_id=subtask.id).order_by('idx') + all_test_cases.extend(test_cases) + + total_cases = len(all_test_cases) + score_per_case = submission.max_score // total_cases if total_cases > 0 else 0 + + for test_case in all_test_cases: + SubmissionResult.objects.update_or_create( + submission=submission, + subtask_id=test_case.subtask_id.id, + test_case_index=test_case.idx, + defaults={ + 'problem_id': submission.problem_id, + 'test_case_id': None, + 'status': 'accepted', + 'execution_time': execution_time // total_cases if total_cases > 0 else execution_time, + 'memory_usage': memory_usage, + 'score': score_per_case, + 'max_score': score_per_case, + 'error_message': None, + } + ) + + logger.info(f'Created {len(all_test_cases)} accepted results for all test cases of submission {submission_id}') + else: + # 正常情況:處理每個測資結果 + logger.info(f'Processing {len(test_results)} test results normally') + + if len(test_results) == 0: + logger.warning(f'Submission {submission_id} has status={judge_status} but test_results is empty!') + + for test_result in test_results: + # 轉換 status 為字串格式(SubmissionResult 使用字串) + result_status = test_result.get('status', 'runtime_error') + + # 重要:Sandbox 文件中的欄位映射 + # - Sandbox 的 test_case_id → 實際是 subtask_no(第幾個 subtask) + # - Sandbox 的 test_case_index → 測資編號(相對於 subtask) + subtask_no = test_result.get('test_case_id') # Sandbox 用這個欄位傳 subtask 編號 + test_case_index = test_result.get('test_case_index', 1) # 測資編號 + + logger.info(f'Processing test_result: subtask_no={subtask_no} (type={type(subtask_no)}), test_case_index={test_case_index}, status={result_status}') + + if subtask_no is None: + logger.warning(f'Missing subtask_no (test_case_id) in test_result') + continue + + # 容錯:嘗試轉換 subtask_no 為整數(防止型別不匹配) + try: + subtask_no = int(subtask_no) + except (ValueError, TypeError): + logger.error(f'Invalid subtask_no format: {subtask_no}') + continue + + # 根據 subtask_no 找到實際的 subtask_id + try: + subtask = Problem_subtasks.objects.get( + problem_id=submission.problem_id, + subtask_no=subtask_no + ) + subtask_id = subtask.id + logger.info(f'Found subtask: subtask_no={subtask_no} -> subtask_id={subtask_id}') + except Problem_subtasks.DoesNotExist: + # 容錯:如果找不到,嘗試 subtask_no+1(因為 Sandbox 可能從 0 開始) + try: + logger.warning(f'Subtask not found with subtask_no={subtask_no}, trying subtask_no={subtask_no+1}') + subtask = Problem_subtasks.objects.get( + problem_id=submission.problem_id, + subtask_no=subtask_no + 1 + ) + subtask_id = subtask.id + logger.info(f'Found subtask with adjusted index: subtask_no={subtask_no+1} -> subtask_id={subtask_id}') + except Problem_subtasks.DoesNotExist: + logger.error(f'Subtask not found even with adjusted index: problem_id={submission.problem_id}, subtask_no={subtask_no} or {subtask_no+1}') + continue + + # 創建或更新記錄 + result, created = SubmissionResult.objects.update_or_create( + submission=submission, + subtask_id=subtask_id, + test_case_index=test_case_index, + defaults={ + 'problem_id': submission.problem_id, + 'test_case_id': None, # 實際的 test_case_id 不使用 + 'status': result_status, + 'execution_time': test_result.get('execution_time', 0), + 'memory_usage': test_result.get('memory_usage', 0), + 'score': test_result.get('score', 0), + 'max_score': test_result.get('max_score', 100), + 'error_message': test_result.get('error_message'), + } + ) + logger.info(f'SubmissionResult {"created" if created else "updated"}: subtask_id={subtask_id}, test_case_index={test_case_index}') + + logger.info(f'Created {len(test_results)} test results for submission {submission_id}') # 5. 更新 UserProblemSolveStatus(全域層級) update_user_problem_stats(submission) diff --git a/user/management/commands/seed_data.py b/user/management/commands/seed_data.py index 94b8a17..c19e07e 100644 --- a/user/management/commands/seed_data.py +++ b/user/management/commands/seed_data.py @@ -767,8 +767,9 @@ def create_submission_results(self, submission, problem, overall_status): SubmissionResult.objects.create( problem_id=problem.id, submission=submission, + subtask_id=test_case.subtask_id.id, # 必填:subtask 的資料庫 ID test_case_id=test_case.id, - test_case_index=idx, + test_case_index=test_case.idx, # 使用實際的 test_case.idx 而不是 enumerate 的 idx status=tc_status, execution_time=random.randint(10, 500) if tc_status != 'time_limit_exceeded' else random.randint(1000, 3000), memory_usage=random.randint(256000, 512000) if tc_status == 'memory_limit_exceeded' else random.randint(1000, 30000),