final-vibevault-template/.autograde/grade_grouped.py
2025-12-01 22:12:02 +08:00

208 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
分组编程题评分脚本
解析 JUnit XML 报告按测试分组Core/Advanced/Challenge计算加权分数
"""
import argparse
import xml.etree.ElementTree as ET
import json
import os
import re
import sys
from pathlib import Path
from glob import glob
def parse_junit_files(junit_dir):
"""
解析目录下所有 JUnit XML 报告
Returns
-------
results : list of dict
每个测试的结果,包含 classname, name, passed
"""
results = []
xml_files = glob(os.path.join(junit_dir, "TEST-*.xml"))
if not xml_files:
xml_files = glob(os.path.join(junit_dir, "*.xml"))
for xml_file in xml_files:
try:
root = ET.parse(xml_file).getroot()
for testsuite in root.iter("testsuite"):
for testcase in testsuite.iter("testcase"):
classname = testcase.get("classname", "")
name = testcase.get("name", "")
# 检查是否有 failure、error 或 skipped 子元素
failed = any(testcase.iter("failure")) or any(testcase.iter("error"))
skipped = any(testcase.iter("skipped"))
results.append({
"classname": classname,
"name": name,
"passed": not failed and not skipped,
"skipped": skipped
})
except Exception as e:
print(f"Error parsing {xml_file}: {e}", file=sys.stderr)
return results
def load_groups_config(groups_file):
"""加载测试分组配置"""
if not os.path.exists(groups_file):
# 默认配置
return {
"groups": {
"core": {"pattern": ".*core.*", "weight": 0.75, "max_score": 60},
"advanced": {"pattern": ".*advanced.*", "weight": 0.125, "max_score": 10},
"challenge": {"pattern": ".*challenge.*", "weight": 0.125, "max_score": 10}
},
"fallback_group": "core"
}
with open(groups_file, "r", encoding="utf-8") as f:
return json.load(f)
def categorize_test(classname, groups_config):
"""根据 classname 将测试分类到对应的组"""
for group_name, group_info in groups_config.get("groups", {}).items():
pattern = group_info.get("pattern", "")
if re.search(pattern, classname, re.IGNORECASE):
return group_name
return groups_config.get("fallback_group", "core")
def calculate_grouped_score(test_results, groups_config):
"""
按分组计算加权分数
Returns
-------
dict
包含各组得分和总分的字典
"""
groups = groups_config.get("groups", {})
# 初始化各组统计
group_stats = {}
for group_name, group_info in groups.items():
group_stats[group_name] = {
"passed": 0,
"total": 0,
"max_score": group_info.get("max_score", 10),
"weight": group_info.get("weight", 0.1),
"tests": []
}
# 分类并统计测试结果
for test in test_results:
group = categorize_test(test["classname"], groups_config)
if group not in group_stats:
group = groups_config.get("fallback_group", "core")
group_stats[group]["total"] += 1
if test["passed"]:
group_stats[group]["passed"] += 1
else:
group_stats[group]["tests"].append(f"{test['classname']}.{test['name']}")
# 计算各组得分
total_score = 0
group_scores = {}
for group_name, stats in group_stats.items():
if stats["total"] > 0:
pass_rate = stats["passed"] / stats["total"]
group_score = pass_rate * stats["max_score"]
else:
group_score = 0
group_scores[group_name] = {
"passed": stats["passed"],
"total": stats["total"],
"max_score": stats["max_score"],
"score": round(group_score, 2),
"failed_tests": stats["tests"][:10] # 只保留前 10 个失败测试
}
total_score += group_score
return {
"total_score": round(total_score, 2),
"max_score": 80, # 编程测试总分 80 分Core 60 + Advanced 10 + Challenge 10
"groups": group_scores
}
def main():
parser = argparse.ArgumentParser(description="Grade programming assignments with test groups")
parser.add_argument("--junit-dir", required=True, help="Directory containing JUnit XML files")
parser.add_argument("--groups", default="test_groups.json", help="Test groups configuration file")
parser.add_argument("--out", default="grade.json", help="Output JSON file")
parser.add_argument("--summary", default="summary.md", help="Output summary markdown file")
args = parser.parse_args()
# 解析测试结果
test_results = parse_junit_files(args.junit_dir)
if not test_results:
print("Warning: No test results found", file=sys.stderr)
grade_data = {
"total_score": 0,
"max_score": 80,
"groups": {},
"error": "No test results found"
}
else:
# 加载分组配置
groups_config = load_groups_config(args.groups)
# 计算分组分数
grade_data = calculate_grouped_score(test_results, groups_config)
# 保存 grade.json
with open(args.out, "w", encoding="utf-8") as f:
json.dump(grade_data, f, ensure_ascii=False, indent=2)
# 生成 summary.md
with open(args.summary, "w", encoding="utf-8") as f:
f.write("# 编程测试成绩报告\n\n")
f.write(f"**总分:{grade_data['total_score']:.2f} / {grade_data['max_score']}**\n\n")
f.write("## 分组得分\n\n")
f.write("| 分组 | 通过 | 总数 | 得分 | 满分 |\n")
f.write("|------|------|------|------|------|\n")
for group_name, group_info in grade_data.get("groups", {}).items():
f.write(f"| {group_name} | {group_info['passed']} | {group_info['total']} | "
f"{group_info['score']:.2f} | {group_info['max_score']} |\n")
# 列出失败的测试
all_failed = []
for group_name, group_info in grade_data.get("groups", {}).items():
all_failed.extend(group_info.get("failed_tests", []))
if all_failed:
f.write("\n## 未通过的测试\n\n")
for test in all_failed[:20]: # 最多显示 20 个
f.write(f"- {test}\n")
if len(all_failed) > 20:
f.write(f"\n... 还有 {len(all_failed) - 20} 个未通过的测试\n")
print(f"Grading complete: {grade_data['total_score']:.2f}/{grade_data['max_score']}")
if __name__ == "__main__":
main()