ebe33392ca
Preserve document revisions and evidence, harden indexing and export behavior, add explicit material selection and direct-folder import, validate model JSON schemas with safe error messages, and retain student selection while loading scoring data.
68 lines
3.3 KiB
Python
68 lines
3.3 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from sqlalchemy import select, text
|
|
|
|
from wisegrade.models import CourseObjective, CourseVersion, GraduationIndicator, RubricCriterion, RubricScoreLevel, RubricVersion, TopicDirection
|
|
from wisegrade.rules import RubricData, totals
|
|
from wisegrade.storage import classify, extract_identity, parse_student_folder, secure_path, sha256
|
|
|
|
|
|
def test_postgresql_migrations_and_vector_extension(db):
|
|
assert db.bind.dialect.name == "postgresql"
|
|
assert db.scalar(text("SELECT version_num FROM alembic_version")) == "0009"
|
|
assert db.scalar(text("SELECT extname FROM pg_extension WHERE extname = 'vector'")) == "vector"
|
|
|
|
|
|
def test_seeded_course_and_weights(db):
|
|
version = db.scalar(select(CourseVersion))
|
|
rubric = db.scalar(select(RubricVersion))
|
|
assert version.published and rubric.published
|
|
assert {item["code"] for item in version.indicators} == {"2.3", "2.4", "4.1", "4.2", "4.3", "4.4", "11.1", "11.2"}
|
|
assert len(RubricData(criteria=rubric.criteria, weights=rubric.weights).criteria) == 22
|
|
assert rubric.weights == {"stage1": .1, "stage2": .1, "stage3": .1, "final": .7}
|
|
assert len(db.scalars(select(CourseObjective)).all()) == 4
|
|
assert len(db.scalars(select(GraduationIndicator)).all()) == 8
|
|
assert len(db.scalars(select(TopicDirection)).all()) == 3
|
|
assert len(db.scalars(select(RubricCriterion)).all()) == 22
|
|
assert len(db.scalars(select(RubricScoreLevel)).all()) == 66
|
|
|
|
|
|
def test_score_totals_preserve_missing(db):
|
|
rubric = db.scalar(select(RubricVersion))
|
|
scores = {item["criterion_id"]: item["max_score"] for item in rubric.criteria}
|
|
assert totals(scores, rubric.criteria, rubric.weights)["overall"] == 100
|
|
scores.pop("stage2.hardware")
|
|
result = totals(scores, rubric.criteria, rubric.weights)
|
|
assert result["stage2"] is None and result["overall"] is None
|
|
|
|
|
|
def test_invalid_rubric_weights(db):
|
|
rubric = db.scalar(select(RubricVersion))
|
|
with pytest.raises(ValueError):
|
|
RubricData(criteria=rubric.criteria, weights={"stage1": .2, "stage2": .1, "stage3": .1, "final": .7})
|
|
|
|
|
|
def test_path_security_and_hash(environment):
|
|
root = environment / "inbox"
|
|
target = root / "student" / "report.pdf"
|
|
target.parent.mkdir()
|
|
target.write_bytes(b"unchanged")
|
|
assert secure_path("student/report.pdf", root=root) == target
|
|
assert sha256(target) == "aaa8d3c8d74ad3e8f6b1772aa9c7e0eaa528cb42fc93599ce2f125b00d4c424c"
|
|
for bad in ("../outside", "/etc/passwd", "student/../../outside", "student\\report.pdf"):
|
|
with pytest.raises(ValueError):
|
|
secure_path(bad, root=root)
|
|
(root / "link").symlink_to(environment)
|
|
with pytest.raises(ValueError):
|
|
secure_path("link/test.db", root=root, must_exist=False)
|
|
|
|
|
|
def test_student_folder_and_file_recognition():
|
|
assert parse_student_folder("1023009006_李傲琦_基于ROS2系统的自瞄移动机器人")["topic"].startswith("基于ROS2")
|
|
assert parse_student_folder("bad_name")["status"] == "needs_confirmation"
|
|
assert classify(Path("汇报PPT.pdf")) == ("presentation", "needs_confirmation")
|
|
assert classify(Path("阶段报告2.docx")) == ("stage2", "recognized")
|
|
assert classify(Path("legacy.doc")) == ("unknown", "unsupported")
|
|
assert extract_identity("学号:1023009006\n姓名:李傲琦\n题目:机器人系统")["topic"] == "机器人系统"
|