File size: 5,870 Bytes
05b4327
 
 
 
 
 
 
 
 
 
 
629b5a3
 
 
 
05b4327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629b5a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import math
import pytest
from enum import Enum

from app.services.answer_utils import (
    normalize_question_type,
    parse_float,
    parse_nat_range,
    split_answer_tokens,
    is_valid_nat_answer,
    normalize_choice_answer,
    validate_msq_answer,
    validate_mcq_answer,
    validate_nat_answer,
    validate_answer_for_type,
)

class DummyEnum(Enum):
    MCQ = "MCQ"
    NAT = "QuestionType.NAT"

def test_normalize_question_type():
    assert normalize_question_type(" MCQ ") == "mcq"
    assert normalize_question_type("NAT") == "nat"
    assert normalize_question_type("QuestionType.MSQ") == "msq"
    assert normalize_question_type(DummyEnum.MCQ) == "mcq"
    assert normalize_question_type(DummyEnum.NAT) == "nat"
    assert normalize_question_type(None) == ""
    assert normalize_question_type("") == ""

def test_parse_float():
    assert parse_float("1.23") == 1.23
    assert parse_float("-0.5") == -0.5
    assert parse_float("1e-5") == 1e-5
    assert parse_float(42) == 42.0
    
    # Invalid strings
    assert parse_float("abc") is None
    assert parse_float("1.2.3") is None
    assert parse_float("") is None
    assert parse_float(None) is None
    
    # Non-finite values
    assert parse_float(float("inf")) is None
    assert parse_float(float("-inf")) is None
    assert parse_float(float("nan")) is None

def test_parse_nat_range():
    assert parse_nat_range("1-2") == (1.0, 2.0)
    assert parse_nat_range("1.5 : 2.5") == (1.5, 2.5)
    assert parse_nat_range("  10  to  20  ") == (10.0, 20.0)
    assert parse_nat_range("-2--1") == (-2.0, -1.0)
    assert parse_nat_range("-5 : -3.5") == (-5.0, -3.5)
    
    # Invalid ranges (lo > hi)
    assert parse_nat_range("2-1") is None
    assert parse_nat_range("5 to 3") is None
    
    # Invalid formats
    assert parse_nat_range("1-2-3") is None
    assert parse_nat_range("abc-def") is None
    assert parse_nat_range("1") is None
    assert parse_nat_range(None) is None

def test_split_answer_tokens():
    assert split_answer_tokens("A, B, C") == ["A", "B", "C"]
    assert split_answer_tokens("A ; B / C") == ["A", "B", "C"]
    assert split_answer_tokens("  word1 , word2  ") == ["word1", "word2"]
    assert split_answer_tokens("A") == ["A"]
    assert split_answer_tokens("") == []
    assert split_answer_tokens("   ") == []
    assert split_answer_tokens(None) == []
    assert split_answer_tokens(",;/") == []

def test_is_valid_nat_answer():
    assert is_valid_nat_answer("1.5") is True
    assert is_valid_nat_answer("1.5:2.5") is True
    assert is_valid_nat_answer("-10 to -5") is True
    assert is_valid_nat_answer("1.5, 2.5:3.5, 4") is True
    
    assert is_valid_nat_answer("abc") is False
    assert is_valid_nat_answer("1.5, abc") is False
    assert is_valid_nat_answer("") is False
    assert is_valid_nat_answer(None) is False
    assert is_valid_nat_answer("2-1") is False # Invalid range (lo > hi)
    assert is_valid_nat_answer("1.5-2.5-3.5") is False

def test_normalize_choice_answer():
    assert normalize_choice_answer("a, b, c") == "A,B,C"
    assert normalize_choice_answer(" a ; B /  c ") == "A,B,C"
    assert normalize_choice_answer("option1") == "OPTION1"
    assert normalize_choice_answer("  ") == ""
    assert normalize_choice_answer(None) == ""

from app.models.models import Question
from app.services.scoring import evaluate_answer

def test_nat_colon_range_scores_boundary_values():
    question = Question(
        question_type="nat",
        question_text="Probability",
        options=[],
        correct_answer="0.44:0.45",
        marks=2.0,
        negative_marks=0.0,
    )

    for selected in ("0.44", "0.445", "0.45"):
        assert evaluate_answer(question, selected) == (True, 2.0)

    assert evaluate_answer(question, "0.46") == (False, 0.0)

def test_nat_hyphen_negative_range_still_works():
    assert parse_nat_range("-2--1") == (-2.0, -1.0)
    assert is_valid_nat_answer("-2--1") is True

def test_validate_mcq_answer():
    assert validate_mcq_answer("A") == "A"
    assert validate_mcq_answer("C") == "C"

    with pytest.raises(ValueError, match="MCQ correct_answer must be one of A, B, C, or D"):
        validate_mcq_answer("E")
    
    with pytest.raises(ValueError, match="MCQ correct_answer must be one of A, B, C, or D"):
        validate_mcq_answer("A,B")

def test_validate_msq_answer():
    # Happy paths
    assert validate_msq_answer("A, B") == ("A,B", 0.0)
    assert validate_msq_answer(" a ; C / d ") == ("A,C,D", 0.0)
    assert validate_msq_answer("A") == ("A", 0.0)
    
    # Duplicates are removed (order preserved)
    assert validate_msq_answer("A, B, A, C, b") == ("A,B,C", 0.0)
    
    # Error cases
    with pytest.raises(ValueError, match="MSQ correct_answer must contain option letters like A,C"):
        validate_msq_answer("")
        
    with pytest.raises(ValueError, match="MSQ correct_answer must contain option letters like A,C"):
        validate_msq_answer("A, E")
        
    with pytest.raises(ValueError, match="MSQ correct_answer must contain option letters like A,C"):
        validate_msq_answer("1, 2")
def test_validate_nat_answer():
    assert validate_nat_answer("1.5:2.5") == 0.0
    assert validate_nat_answer("42") == 0.0

    with pytest.raises(ValueError, match="NAT correct_answer must be a number or range like 41.5-42.5 or 41.5:42.5"):
        validate_nat_answer("abc")

def test_validate_answer_for_type():
    # MCQ
    ans, neg, clear = validate_answer_for_type("mcq", "B")
    assert ans == "B"
    assert neg is None
    assert clear is False

    # MSQ
    ans, neg, clear = validate_answer_for_type("msq", "A, C")
    assert ans == "A,C"
    assert neg == 0.0
    assert clear is False

    # NAT
    ans, neg, clear = validate_answer_for_type("nat", "1.5:2.5")
    assert ans == "1.5:2.5"
    assert neg == 0.0
    assert clear is True