File size: 9,739 Bytes
408204a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Tests for GestureScorer β€” the quality scoring system.

╔══════════════════════════════════════════════════════════════════════════════╗
β•‘  πŸ“š CONCEPT: Unit Testing with unittest                                      β•‘
╠══════════════════════════════════════════════════════════════════════════════╣
β•‘                                                                              β•‘
β•‘  Python's built-in unittest module provides everything we need:             β•‘
β•‘                                                                              β•‘
β•‘  β€’ unittest.TestCase   β€” Base class for test classes                        β•‘
β•‘  β€’ self.assertEqual()  β€” Check two values are equal                         β•‘
β•‘  β€’ self.assertTrue()   β€” Check a condition is True                          β•‘
β•‘  β€’ self.assertIsNone() β€” Check a value is None                              β•‘
β•‘  β€’ self.assertGreater() β€” Check a > b                                       β•‘
β•‘  β€’ self.assertAlmostEqual() β€” Check floats are close (avoids rounding)     β•‘
β•‘                                                                              β•‘
β•‘  Each test method name must start with "test_".                             β•‘
β•‘  Each test checks ONE specific behavior.                                    β•‘
β•‘                                                                              β•‘
β•‘  HOW TO RUN:                                                                 β•‘
β•‘      python -m unittest tests.test_scoring -v                                β•‘
β•‘                                                                              β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
"""

import unittest

from oop_sorting_teaching.models.gesture import GestureImage, GestureRanking
from oop_sorting_teaching.models.scoring import GestureScorer


# ==============================================================================
# Helper: Create a GestureImage with known values
# ==============================================================================

def make_gesture(name: str, capture_id: int, confidence: float) -> GestureImage:
    """
    Factory helper for creating test gestures.

    Using a helper keeps tests short and focused on WHAT is tested,
    not on HOW to construct objects.
    """
    return GestureImage.create_from_prediction(
        gesture_name=name,
        capture_id=capture_id,
        confidence=confidence,
    )


# ==============================================================================
# Tests for compute_score
# ==============================================================================

class TestComputeScore(unittest.TestCase):
    """
    Tests for GestureScorer.compute_score().

    πŸ“š OOP NOTE:
        By inheriting from unittest.TestCase, our class gains
        all the assertion methods (assertEqual, assertTrue, etc.)
        without writing them ourselves. This is INHERITANCE in action!
    """

    def test_basic_fist(self):
        """Fist (complexity 1.0) at 90% confidence β†’ score = 90.0."""
        gesture = make_gesture("fist", 1, confidence=0.90)
        score = GestureScorer.compute_score(gesture)
        self.assertEqual(score, 90.0)

    def test_basic_ok(self):
        """OK (complexity 1.7) at 80% confidence β†’ score = 136.0."""
        gesture = make_gesture("ok", 2, confidence=0.80)
        score = GestureScorer.compute_score(gesture)
        self.assertEqual(score, 136.0)

    def test_zero_confidence(self):
        """Zero confidence should always produce a score of 0."""
        gesture = make_gesture("call", 3, confidence=0.0)
        score = GestureScorer.compute_score(gesture)
        self.assertEqual(score, 0.0)

    def test_full_confidence_fist(self):
        """100% confidence on fist (complexity 1.0) β†’ exactly 100."""
        gesture = make_gesture("fist", 4, confidence=1.0)
        score = GestureScorer.compute_score(gesture)
        self.assertEqual(score, 100.0)

    def test_mute_highest_complexity(self):
        """Mute (complexity 2.0) at 100% confidence β†’ 200.0."""
        gesture = make_gesture("mute", 5, confidence=1.0)
        score = GestureScorer.compute_score(gesture)
        self.assertEqual(score, 200.0)

    def test_confidence_weight_doubles_score(self):
        """Doubling confidence_weight should double the score."""
        gesture = make_gesture("fist", 6, confidence=0.50)
        normal = GestureScorer.compute_score(gesture, confidence_weight=1.0)
        doubled = GestureScorer.compute_score(gesture, confidence_weight=2.0)
        # assertAlmostEqual handles floating-point precision gracefully
        self.assertAlmostEqual(doubled, normal * 2, places=5)

    def test_complexity_weight_changes_score(self):
        """Higher complexity_weight should amplify complexity differences."""
        gesture = make_gesture("ok", 7, confidence=1.0)
        # complexity for ok = 1.7
        low_weight = GestureScorer.compute_score(
            gesture, complexity_weight=0.5
        )
        high_weight = GestureScorer.compute_score(
            gesture, complexity_weight=2.0
        )
        # 1.7^0.5 β‰ˆ 1.304, 1.7^2.0 = 2.89 β†’ high > low
        self.assertGreater(high_weight, low_weight)

    def test_score_is_rounded(self):
        """Score should be rounded to 2 decimal places."""
        gesture = make_gesture("rock", 8, confidence=0.777)
        score = GestureScorer.compute_score(gesture)
        # 0.777 * 100 * 1.6 = 124.32
        self.assertEqual(score, 124.32)


# ==============================================================================
# Tests for apply_scores and clear_scores
# ==============================================================================

class TestApplyAndClearScores(unittest.TestCase):
    """Tests for applying and clearing scores on gesture lists."""

    def test_apply_scores_sets_sort_value(self):
        """After apply_scores, every gesture should have _sort_value set."""
        images = [
            make_gesture("fist", 1, 0.9),
            make_gesture("ok", 2, 0.8),
            make_gesture("call", 3, 0.7),
        ]
        GestureScorer.apply_scores(images)
        for img in images:
            self.assertIsNotNone(img._sort_value)

    def test_apply_scores_negates(self):
        """_sort_value should be negative (for descending sort order)."""
        images = [make_gesture("fist", 1, 0.9)]
        GestureScorer.apply_scores(images)
        self.assertLess(images[0]._sort_value, 0)

    def test_apply_scores_correct_value(self):
        """_sort_value should equal the negated computed score."""
        gesture = make_gesture("fist", 1, 0.9)
        expected_score = GestureScorer.compute_score(gesture)
        GestureScorer.apply_scores([gesture])
        self.assertAlmostEqual(gesture._sort_value, -expected_score, places=5)

    def test_clear_scores_resets_to_none(self):
        """After clear_scores, _sort_value should be None."""
        images = [
            make_gesture("fist", 1, 0.9),
            make_gesture("ok", 2, 0.8),
        ]
        GestureScorer.apply_scores(images)
        GestureScorer.clear_scores(images)
        for img in images:
            self.assertIsNone(img._sort_value)

    def test_get_score_returns_positive(self):
        """get_score should return the positive (display) score."""
        gesture = make_gesture("ok", 1, 0.8)
        GestureScorer.apply_scores([gesture])
        display_score = GestureScorer.get_score(gesture)
        self.assertGreater(display_score, 0)
        self.assertEqual(display_score, GestureScorer.compute_score(gesture))

    def test_get_score_returns_zero_when_no_score(self):
        """get_score returns 0.0 when no score has been applied."""
        gesture = make_gesture("fist", 1, 0.9)
        self.assertEqual(GestureScorer.get_score(gesture), 0.0)


# ==============================================================================
# Tests for get_formula_description
# ==============================================================================

class TestFormulaDescription(unittest.TestCase):
    """Tests for the human-readable formula string."""

    def test_default_formula(self):
        """Default weights produce a readable formula string."""
        desc = GestureScorer.get_formula_description()
        self.assertIn("confidence", desc)
        self.assertIn("complexity", desc)
        self.assertIn("1.0", desc)

    def test_custom_weights_in_formula(self):
        """Custom weights appear in the formula description."""
        desc = GestureScorer.get_formula_description(2.5, 1.5)
        self.assertIn("2.5", desc)
        self.assertIn("1.5", desc)


# ==============================================================================
# Run tests when this file is executed directly
# ==============================================================================
# πŸ“š This is the standard Python way to make a script runnable:
#     python tests/test_scoring.py       β†’ runs all tests in this file
#     python -m unittest tests.test_scoring -v  β†’ same, with verbose output

if __name__ == "__main__":
    unittest.main()