File size: 9,903 Bytes
7638cbd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""Tests for enum subset validation in REQUIRED_INPUTS.

This module tests the enhanced validation logic that supports restricting
enum fields to specific subsets using Literal types.
"""

from typing import Any, Literal

from sentinel.risk_models.base import RiskModel
from sentinel.user_input import (
    Anthropometrics,
    Demographics,
    Ethnicity,
    Lifestyle,
    PersonalMedicalHistory,
    Sex,
    SmokingHistory,
    SmokingStatus,
    UserInput,
)


class EnumValidationTestModel(RiskModel):
    """Test risk model with various enum restrictions for validation testing."""

    def __init__(self):
        super().__init__("test_enum_validation")

    # Test cases for different enum restriction patterns
    REQUIRED_INPUTS: dict[str, tuple[type | Any, bool]] = {
        # Single enum value restriction
        "demographics.sex": (Literal[Sex.FEMALE], True),
        # Multiple enum value restriction (subset)
        "demographics.ethnicity": (
            Literal[Ethnicity.WHITE, Ethnicity.BLACK, Ethnicity.ASIAN] | None,
            False,
        ),
    }

    def compute_score(self, user: UserInput) -> str:
        """Test implementation.

        Args:
            user: The user profile to score.

        Returns:
            A test score string.
        """
        return "test_score"

    def cancer_type(self) -> str:
        return "test"

    def description(self) -> str:
        return "Test model"

    def interpretation(self) -> str:
        return "Test interpretation"

    def references(self) -> list[str]:
        return ["Test reference"]

    def time_horizon_years(self) -> float | None:
        return None


class TestEnumSubsetValidation:
    """Test enum subset validation functionality."""

    def setup_method(self):
        """Set up test model."""
        self.model = EnumValidationTestModel()

    def _create_user_input(
        self, sex: Sex, ethnicity: Ethnicity | None = None
    ) -> UserInput:
        """Create a valid UserInput instance for testing.

        Args:
            sex: The biological sex for the user.
            ethnicity: The ethnicity for the user (optional).

        Returns:
            A valid UserInput instance for testing.
        """
        return UserInput(
            demographics=Demographics(
                age_years=40,
                sex=sex,
                ethnicity=ethnicity,
                anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0),
            ),
            lifestyle=Lifestyle(
                smoking=SmokingHistory(status=SmokingStatus.NEVER),
            ),
            personal_medical_history=PersonalMedicalHistory(),
        )

    def test_single_enum_value_restriction_valid(self):
        """Test that valid single enum value passes validation."""
        user = self._create_user_input(Sex.FEMALE, Ethnicity.WHITE)

        is_valid, errors = self.model.validate_inputs(user)
        assert is_valid
        assert len(errors) == 0

    def test_single_enum_value_restriction_invalid(self):
        """Test that invalid single enum value fails validation with clear message."""
        user = self._create_user_input(Sex.MALE, Ethnicity.WHITE)  # Should be FEMALE

        is_valid, errors = self.model.validate_inputs(user)
        assert not is_valid
        assert len(errors) == 1
        assert "Field 'demographics.sex': must be FEMALE" in errors[0]

    def test_multiple_enum_value_restriction_valid(self):
        """Test that valid enum values from subset pass validation."""
        valid_ethnicities = [Ethnicity.WHITE, Ethnicity.BLACK, Ethnicity.ASIAN]

        for ethnicity in valid_ethnicities:
            user = self._create_user_input(Sex.FEMALE, ethnicity)

            is_valid, errors = self.model.validate_inputs(user)
            assert is_valid, f"Failed for ethnicity: {ethnicity}"
            assert len(errors) == 0

    def test_multiple_enum_value_restriction_invalid(self):
        """Test that invalid enum values fail validation with clear message."""
        invalid_ethnicities = [
            Ethnicity.HISPANIC,
            Ethnicity.ASHKENAZI_JEWISH,
            Ethnicity.NATIVE_AMERICAN,
            Ethnicity.PACIFIC_ISLANDER,
            Ethnicity.OTHER,
            Ethnicity.UNKNOWN,
        ]

        for ethnicity in invalid_ethnicities:
            user = self._create_user_input(Sex.FEMALE, ethnicity)

            is_valid, errors = self.model.validate_inputs(user)
            assert not is_valid, f"Should have failed for ethnicity: {ethnicity}"
            assert len(errors) == 1
            assert "Field 'demographics.ethnicity': Input should be" in errors[0]
            assert (
                "WHITE" in errors[0] and "BLACK" in errors[0] and "ASIAN" in errors[0]
            )

    def test_optional_enum_field_with_none(self):
        """Test that None values are handled correctly for optional enum fields."""
        user = self._create_user_input(Sex.FEMALE, None)  # Optional field

        is_valid, errors = self.model.validate_inputs(user)
        assert is_valid
        assert len(errors) == 0

    def test_missing_required_enum_field(self):
        """Test that missing required enum fields are caught."""

        # Create a model that requires a field that's not in the user input
        class MissingFieldModel(RiskModel):
            """Test model for missing field validation."""

            def __init__(self):
                super().__init__("missing_field_test")

            REQUIRED_INPUTS: dict[str, tuple[Any, bool]] = {
                "demographics.sex": (Literal[Sex.FEMALE], True),
                "demographics.ethnicity": (Ethnicity | None, False),
                "demographics.nonexistent_field": (
                    str,
                    True,
                ),  # This field doesn't exist
            }

            def compute_score(self, user: UserInput) -> str:
                return "test"

            def cancer_type(self) -> str:
                return "test"

            def description(self) -> str:
                return "test"

            def interpretation(self) -> str:
                return "test"

            def references(self) -> list[str]:
                return ["test"]

            def time_horizon_years(self) -> float | None:
                return None

        model = MissingFieldModel()
        user = self._create_user_input(Sex.FEMALE, Ethnicity.WHITE)

        is_valid, errors = model.validate_inputs(user)
        assert not is_valid
        assert len(errors) == 1
        assert "Required field 'demographics.nonexistent_field' is missing" in errors[0]

    def test_multiple_validation_errors(self):
        """Test that multiple validation errors are reported."""
        user = self._create_user_input(Sex.MALE, Ethnicity.HISPANIC)  # Both wrong

        is_valid, errors = self.model.validate_inputs(user)
        assert not is_valid
        assert len(errors) == 2

        # Check that both errors are present
        error_messages = " ".join(errors)
        assert "must be FEMALE" in error_messages
        assert "Input should be" in error_messages
        assert (
            "WHITE" in error_messages
            and "BLACK" in error_messages
            and "ASIAN" in error_messages
        )

    def test_literal_enum_type_detection(self):
        """Test the _is_literal_enum_type helper method."""
        # Test Literal with single enum value
        single_literal = Literal[Sex.FEMALE]
        assert self.model._is_literal_enum_type(single_literal)

        # Test Literal with multiple enum values
        multi_literal = Literal[Ethnicity.WHITE, Ethnicity.BLACK]
        assert self.model._is_literal_enum_type(multi_literal)

        # Test non-Literal types
        assert not self.model._is_literal_enum_type(Sex)
        assert not self.model._is_literal_enum_type(int)
        assert not self.model._is_literal_enum_type(str)

    def test_extract_literal_enum_values(self):
        """Test the _extract_literal_enum_values helper method."""
        # Test single enum value
        single_literal = Literal[Sex.FEMALE]
        values = self.model._extract_literal_enum_values(single_literal)
        assert values == ["FEMALE"]

        # Test multiple enum values
        multi_literal = Literal[Ethnicity.WHITE, Ethnicity.BLACK, Ethnicity.ASIAN]
        values = self.model._extract_literal_enum_values(multi_literal)
        assert set(values) == {"WHITE", "BLACK", "ASIAN"}

    def test_backward_compatibility_unrestricted_enum(self):
        """Test that unrestricted enum types still work (backward compatibility)."""

        # Create a model with unrestricted enum
        class UnrestrictedModel(RiskModel):
            """Test model for backward compatibility with unrestricted enums."""

            def __init__(self):
                super().__init__("unrestricted_test")

            REQUIRED_INPUTS: dict[str, tuple[type | Any, bool]] = {
                "demographics.sex": (Sex, True),
                "demographics.ethnicity": (Ethnicity | None, False),
            }

            def compute_score(self, user: UserInput) -> str:
                return "test"

            def cancer_type(self) -> str:
                return "test"

            def description(self) -> str:
                return "test"

            def interpretation(self) -> str:
                return "test"

            def references(self) -> list[str]:
                return ["test"]

            def time_horizon_years(self) -> float | None:
                return None

        model = UnrestrictedModel()

        # Test with any valid enum values
        user = self._create_user_input(
            Sex.MALE, Ethnicity.HISPANIC
        )  # Any values should work

        is_valid, errors = model.validate_inputs(user)
        assert is_valid
        assert len(errors) == 0