"""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]