208 lines
6.9 KiB
Python
208 lines
6.9 KiB
Python
#!/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()
|
||
|