final-vibevault-template/.autograde/grade_grouped.py

208 lines
6.9 KiB
Python
Raw Normal View History

2025-12-01 22:12:02 +08:00
#!/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()