Spaces:
Sleeping
Sleeping
| """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 | |
| # --------------------------------------------------------------------------- | |
| def test_normalize_letter(raw, expected): | |
| assert normalize_letter(raw) == expected | |
| # --------------------------------------------------------------------------- | |
| # from_dict validation | |
| # --------------------------------------------------------------------------- | |
| 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") | |
| 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) | |
| 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") | |
| 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) | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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" | |
| 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") | |
| 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 == ("=",) | |
| 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") | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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" | |
| 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 | |
| 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" | |
| 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 | |
| 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 | |
| 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) == [] | |
| 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) | |
| 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] | |