RuslanKain
feat: Implement Gesture Scoring System
408204a
"""
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()