188 lines
5.4 KiB
Python
188 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
编程题评分脚本
|
||
|
||
解析 JUnit XML 报告,计算分数,考虑迟交扣分,生成 grade.json 和 summary.md
|
||
"""
|
||
|
||
import argparse
|
||
import xml.etree.ElementTree as ET
|
||
import json
|
||
import subprocess
|
||
import os
|
||
import time
|
||
import sys
|
||
from pathlib import Path
|
||
from dotenv import load_dotenv
|
||
|
||
# 加载环境变量(支持从 .env 文件或环境变量读取)
|
||
load_dotenv()
|
||
|
||
|
||
def commit_ts():
|
||
"""获取最后一次提交的时间戳(Unix 时间戳)"""
|
||
try:
|
||
out = subprocess.check_output(
|
||
["git", "log", "-1", "--format=%ct"],
|
||
stderr=subprocess.DEVNULL
|
||
).decode().strip()
|
||
return int(out)
|
||
except Exception:
|
||
return int(time.time())
|
||
|
||
|
||
def parse_junit(junit_path):
|
||
"""
|
||
解析 JUnit XML 报告
|
||
|
||
Returns
|
||
-------
|
||
passed : int
|
||
通过的测试数
|
||
total : int
|
||
总测试数
|
||
fails : list
|
||
失败的测试名称列表
|
||
"""
|
||
if not os.path.exists(junit_path):
|
||
return (0, 0, [])
|
||
|
||
try:
|
||
root = ET.parse(junit_path).getroot()
|
||
total = 0
|
||
passed = 0
|
||
fails = []
|
||
|
||
for testsuite in root.iter("testsuite"):
|
||
for testcase in testsuite.iter("testcase"):
|
||
total += 1
|
||
# 检查是否有 failure、error 或 skipped 子元素
|
||
if list(testcase):
|
||
classname = testcase.get("classname", "")
|
||
name = testcase.get("name", "")
|
||
full_name = f"{classname}.{name}" if classname else name
|
||
fails.append(full_name)
|
||
else:
|
||
passed += 1
|
||
|
||
return (passed, total, fails)
|
||
except Exception as e:
|
||
print(f"Error parsing JUnit XML: {e}", file=sys.stderr)
|
||
return (0, 0, [])
|
||
|
||
|
||
def calculate_late_penalty(deadline_str):
|
||
"""
|
||
计算迟交扣分
|
||
|
||
Parameters
|
||
----------
|
||
deadline_str : str
|
||
ISO 格式的截止时间(如 "2025-03-15T23:59:59+08:00")
|
||
|
||
Returns
|
||
-------
|
||
penalty : float
|
||
扣分数(0-30)
|
||
"""
|
||
if not deadline_str:
|
||
return 0.0
|
||
|
||
try:
|
||
# 解析截止时间(支持多种格式)
|
||
deadline_str = deadline_str.strip()
|
||
# 移除时区信息(简化处理)
|
||
if '+' in deadline_str:
|
||
deadline_str = deadline_str.split('+')[0]
|
||
elif 'Z' in deadline_str:
|
||
deadline_str = deadline_str.replace('Z', '')
|
||
|
||
# 解析时间
|
||
if 'T' in deadline_str:
|
||
dl = time.mktime(time.strptime(deadline_str[:19], "%Y-%m-%dT%H:%M:%S"))
|
||
else:
|
||
dl = time.mktime(time.strptime(deadline_str[:19], "%Y-%m-%d %H:%M:%S"))
|
||
|
||
commit_time = commit_ts()
|
||
late_sec = max(0, commit_time - dl)
|
||
days = late_sec / 86400
|
||
|
||
# 扣分规则:第一天 10 分,之后每天 5 分,最多 30 分
|
||
if days > 0:
|
||
penalty = min(30.0, 10.0 + 5.0 * days)
|
||
else:
|
||
penalty = 0.0
|
||
|
||
return round(penalty, 2)
|
||
except Exception as e:
|
||
print(f"Error calculating late penalty: {e}", file=sys.stderr)
|
||
return 0.0
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Grade programming assignments from JUnit XML")
|
||
parser.add_argument("--junit", required=True, help="Path to JUnit XML file")
|
||
parser.add_argument("--out", default="grade.json", help="Output JSON file")
|
||
parser.add_argument("--summary", default="summary.md", help="Output summary markdown file")
|
||
parser.add_argument("--bonus", default=None, help="Optional bonus file (e.g., lintr.rds)")
|
||
args = parser.parse_args()
|
||
|
||
# 解析 JUnit XML
|
||
passed, total, fails = parse_junit(args.junit)
|
||
|
||
# 计算基础分数
|
||
if total > 0:
|
||
base_score = 100.0 * (passed / total)
|
||
else:
|
||
base_score = 0.0
|
||
|
||
# 计算迟交扣分
|
||
deadline = os.getenv("DEADLINE", "")
|
||
penalty = calculate_late_penalty(deadline)
|
||
|
||
# 最终分数
|
||
final_score = max(0.0, round(base_score - penalty, 2))
|
||
|
||
# 生成 grade.json
|
||
grade_data = {
|
||
"score": final_score,
|
||
"base_score": round(base_score, 2),
|
||
"penalty": penalty,
|
||
"passed": passed,
|
||
"total": total,
|
||
"fails": fails,
|
||
"timestamp": int(time.time())
|
||
}
|
||
|
||
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"- **通过用例**:{passed}/{total}\n")
|
||
f.write(f"- **原始分**:{base_score:.2f}/100\n")
|
||
if penalty > 0:
|
||
f.write(f"- **迟交扣分**:-{penalty:.2f}\n")
|
||
f.write(f"- **最终分**:**{final_score:.2f}/100**\n\n")
|
||
|
||
if fails:
|
||
f.write("## 未通过的测试\n\n")
|
||
for fail in fails:
|
||
f.write(f"- {fail}\n")
|
||
f.write("\n")
|
||
|
||
if deadline:
|
||
f.write(f"## 截止时间\n\n")
|
||
f.write(f"- 截止时间:{deadline}\n")
|
||
commit_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(commit_ts()))
|
||
f.write(f"- 提交时间:{commit_time_str}\n")
|
||
|
||
print(f"Grading complete: {final_score:.2f}/100 ({passed}/{total} tests passed)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|