ClassLens / chatkit /backend /tests /test_answer_grid.py
Yu Chen
fix the answer parsing not aligned logic
1ce0b71
Raw
History Blame
11.1 kB
"""Unit tests for answer_grid module."""
from __future__ import annotations
import pytest
from app.answer_grid import (
AnswerGrid,
QuestionMeta,
StudentRow,
diff_student,
from_dict,
normalize_letter,
seed_from_parsed,
to_dict,
)
# ---------------------------------------------------------------------------
# normalize_letter
# ---------------------------------------------------------------------------
@pytest.mark.unit
@pytest.mark.parametrize(
"raw, expected",
[
("A", "A"),
("a", "A"),
(" b ", "B"),
("C)", "C"),
("D) foo", "D"),
("=", "="),
(" = ", "="),
("-", "="),
("–", "="),
("—", "="),
("", None),
(None, None),
(" ", None),
("N/A", None),
("invalid", None),
("AB", None),
],
)
def test_normalize_letter(raw, expected):
assert normalize_letter(raw) == expected
# ---------------------------------------------------------------------------
# from_dict validation
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_from_dict_minimal_valid():
payload = {
"total_questions": 2,
"official_answers": ["A", "B"],
"students": [{"name": "Alice", "answers": ["A", "C"]}],
"questions": [
{"number": 1, "text": "Q1", "options": ["A) x", "B) y"]},
{"number": 2, "text": "Q2", "options": ["A) x", "B) y"]},
],
}
grid = from_dict(payload)
assert grid.total_questions == 2
assert grid.official_answers == ("A", "B")
assert grid.students[0].name == "Alice"
assert grid.students[0].answers == ("A", "C")
@pytest.mark.unit
def test_from_dict_pads_short_student_row():
payload = {
"total_questions": 3,
"official_answers": ["A", "B", "C"],
"students": [{"name": "Alice", "answers": ["A"]}],
"questions": [{"text": "Q"}, {"text": "Q"}, {"text": "Q"}],
}
grid = from_dict(payload)
assert grid.students[0].answers == ("A", None, None)
@pytest.mark.unit
def test_from_dict_truncates_long_student_row():
payload = {
"total_questions": 2,
"official_answers": ["A", "B"],
"students": [{"name": "Alice", "answers": ["A", "B", "C", "D"]}],
"questions": [],
}
grid = from_dict(payload)
assert grid.students[0].answers == ("A", "B")
@pytest.mark.unit
@pytest.mark.parametrize(
"bad_field",
[
{"total_questions": 0},
{"total_questions": -1},
{"total_questions": "three"},
],
)
def test_from_dict_rejects_bad_n(bad_field):
payload = {
"total_questions": 2,
"official_answers": ["A", "B"],
"students": [],
"questions": [],
}
payload.update(bad_field)
with pytest.raises(ValueError):
from_dict(payload)
@pytest.mark.unit
def test_from_dict_default_student_name():
payload = {
"total_questions": 1,
"official_answers": ["A"],
"students": [{"answers": ["A"]}],
"questions": [],
}
grid = from_dict(payload)
assert grid.students[0].name == "Student 1"
# ---------------------------------------------------------------------------
# to_dict roundtrip
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_to_dict_roundtrip():
payload = {
"total_questions": 2,
"official_answers": ["A", "B"],
"students": [{"name": "Alice", "answers": ["A", "C"]}],
"questions": [
{"number": 1, "text": "Q1", "options": ["A) x", "B) y"]},
{"number": 2, "text": "Q2", "options": ["A) x", "B) y"]},
],
}
grid = from_dict(payload)
again = from_dict(to_dict(grid))
assert grid == again
# ---------------------------------------------------------------------------
# seed_from_parsed
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_seed_from_parsed_basic():
questions = {
"questions": [
{"number": 1, "text": "Q1", "options": ["A) a", "B) b"]},
{"number": 2, "text": "Q2", "options": ["A) a", "B) b"]},
]
}
students = {
"students": [
{
"name": "Alice",
"answers": [
{"question_number": 1, "answer": "A"},
{"question_number": 2, "answer": "b"},
],
}
]
}
teachers = {
"answers": [
{"question_number": 1, "correct_answer": "A"},
{"question_number": 2, "correct_answer": "B"},
]
}
grid = seed_from_parsed(questions, students, teachers)
assert grid.total_questions == 2
assert grid.official_answers == ("A", "B")
assert grid.students[0].answers == ("A", "B")
assert grid.questions[0].text == "Q1"
@pytest.mark.unit
def test_seed_from_parsed_legacy_dash_becomes_correct_marker():
"""Legacy '-' in a student answer normalizes to the canonical '=' marker."""
questions = {"questions": [{"number": 1, "text": "Q"}, {"number": 2, "text": "Q"}]}
students = {
"students": [
{
"name": "Alice",
"answers": [
{"question_number": 1, "answer": "-"},
{"question_number": 2, "answer": "D"},
],
}
]
}
teachers = {
"answers": [
{"question_number": 1, "correct_answer": "A"},
{"question_number": 2, "correct_answer": "B"},
]
}
grid = seed_from_parsed(questions, students, teachers)
assert grid.students[0].answers == ("=", "D")
@pytest.mark.unit
def test_seed_from_parsed_preserves_explicit_equals():
questions = {"questions": [{"number": 1, "text": "Q"}]}
students = {
"students": [
{"name": "Alice", "answers": [{"question_number": 1, "answer": "="}]}
]
}
teachers = {"answers": [{"question_number": 1, "correct_answer": "A"}]}
grid = seed_from_parsed(questions, students, teachers)
assert grid.students[0].answers == ("=",)
@pytest.mark.unit
def test_seed_from_parsed_no_questions_derives_n_from_teacher():
questions = {}
students = {
"students": [{"name": "A", "answers": [{"question_number": 1, "answer": "A"}]}]
}
teachers = {
"answers": [
{"question_number": 1, "correct_answer": "A"},
{"question_number": 2, "correct_answer": "B"},
{"question_number": 3, "correct_answer": "C"},
]
}
grid = seed_from_parsed(questions, students, teachers)
assert grid.total_questions == 3
assert grid.official_answers == ("A", "B", "C")
@pytest.mark.unit
def test_seed_from_parsed_all_empty_gives_n_one():
grid = seed_from_parsed({}, {}, {})
assert grid.total_questions == 1
assert grid.students == ()
assert grid.official_answers == (None,)
# ---------------------------------------------------------------------------
# diff_student
# ---------------------------------------------------------------------------
@pytest.mark.unit
def test_diff_student_returns_only_wrong():
grid = AnswerGrid(
total_questions=3,
official_answers=("A", "B", "C"),
students=(StudentRow(name="Alice", answers=("A", "D", "C")),),
questions=(
QuestionMeta(number=1, text="Q1", options=()),
QuestionMeta(number=2, text="Q2", options=()),
QuestionMeta(number=3, text="Q3", options=()),
),
)
wrongs = diff_student(grid, 0)
assert len(wrongs) == 1
assert wrongs[0].question.number == 2
assert wrongs[0].student_answer == "D"
assert wrongs[0].correct_answer == "B"
@pytest.mark.unit
def test_diff_student_blank_student_answer_is_wrong():
grid = AnswerGrid(
total_questions=2,
official_answers=("A", "B"),
students=(StudentRow(name="Alice", answers=("A", None)),),
questions=(
QuestionMeta(number=1, text="Q1", options=()),
QuestionMeta(number=2, text="Q2", options=()),
),
)
wrongs = diff_student(grid, 0)
assert len(wrongs) == 1
assert wrongs[0].student_answer is None
@pytest.mark.unit
def test_diff_student_equals_marker_is_correct():
"""A '=' cell means 'student answered correctly' and must never appear in wrongs."""
grid = AnswerGrid(
total_questions=3,
official_answers=("A", "B", "C"),
students=(StudentRow(name="Alice", answers=("=", "X", "=")),),
questions=tuple(
QuestionMeta(number=i + 1, text=f"Q{i+1}", options=()) for i in range(3)
),
)
wrongs = diff_student(grid, 0)
assert len(wrongs) == 1
assert wrongs[0].question.number == 2
assert wrongs[0].student_answer == "X"
@pytest.mark.unit
def test_diff_student_skips_equals_in_official():
grid = AnswerGrid(
total_questions=2,
official_answers=("A", "="), # Q2 has no authoritative key
students=(StudentRow(name="Alice", answers=("X", "Y")),),
questions=(
QuestionMeta(number=1, text="Q1", options=()),
QuestionMeta(number=2, text="Q2", options=()),
),
)
wrongs = diff_student(grid, 0)
assert len(wrongs) == 1
assert wrongs[0].question.number == 1
@pytest.mark.unit
def test_diff_student_skips_blank_official_key():
grid = AnswerGrid(
total_questions=2,
official_answers=("A", None), # Q2 key missing
students=(StudentRow(name="Alice", answers=("B", "C")),),
questions=(
QuestionMeta(number=1, text="Q1", options=()),
QuestionMeta(number=2, text="Q2", options=()),
),
)
wrongs = diff_student(grid, 0)
assert len(wrongs) == 1
assert wrongs[0].question.number == 1
@pytest.mark.unit
def test_diff_student_all_correct():
grid = AnswerGrid(
total_questions=2,
official_answers=("A", "B"),
students=(StudentRow(name="Alice", answers=("A", "B")),),
questions=(
QuestionMeta(number=1, text="Q1", options=()),
QuestionMeta(number=2, text="Q2", options=()),
),
)
assert diff_student(grid, 0) == []
@pytest.mark.unit
def test_diff_student_out_of_range():
grid = AnswerGrid(
total_questions=1,
official_answers=("A",),
students=(),
questions=(QuestionMeta(number=1, text="Q", options=()),),
)
with pytest.raises(IndexError):
diff_student(grid, 0)
@pytest.mark.unit
def test_diff_student_preserves_question_order():
grid = AnswerGrid(
total_questions=4,
official_answers=("A", "B", "C", "D"),
students=(StudentRow(name="Alice", answers=("X", "B", "Y", "Z")),),
questions=tuple(
QuestionMeta(number=i + 1, text=f"Q{i+1}", options=()) for i in range(4)
),
)
wrongs = diff_student(grid, 0)
assert [w.question.number for w in wrongs] == [1, 3, 4]