Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CodeQL Clean Code Analysis

on:
push:
branches: [main, "feature/**", "new-feature/**"]
pull_request:
branches: [main]

jobs:
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4

- name: Checkout CodeQL queries
uses: actions/checkout@v4
with:
repository: HighFive-SWE/codeQL
path: codeql-queries
token: ${{ secrets.GITHUB_TOKEN }}

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: python
config: |
queries:
- uses: ./codeql-queries/.codeql/queries/too-many-parameters.ql
- uses: ./codeql-queries/.codeql/queries/long-function.ql
- uses: ./codeql-queries/.codeql/queries/missing-public-docstring.ql
- uses: ./codeql-queries/.codeql/queries/magic-numbers.ql

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: clean-code
Empty file added tests/__init__.py
Empty file.
135 changes: 135 additions & 0 deletions tests/test_comparator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import numpy as np
import pytest

from vision.core.comparator import (
CORRECT,
PARTIAL,
CompareResult,
_band,
compare_gesture,
)
from vision.gestures.samples import GESTURES


# ── _band ─────────────────────────────────────────────────────────────────────

def test_band_at_correct_threshold_is_correct():
assert _band(CORRECT) == "correct"


def test_band_above_correct_is_correct():
assert _band(CORRECT + 0.01) == "correct"


def test_band_between_partial_and_correct_is_partial():
midpoint = (PARTIAL + CORRECT) / 2
assert _band(midpoint) == "partial"


def test_band_at_partial_threshold_is_partial():
assert _band(PARTIAL) == "partial"


def test_band_just_below_partial_is_incorrect():
assert _band(PARTIAL - 0.001) == "incorrect"


def test_band_zero_is_incorrect():
assert _band(0.0) == "incorrect"


# ── compare_gesture ───────────────────────────────────────────────────────────

def test_compare_identical_inputs_high_accuracy():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
assert result.accuracy > 0.9


def test_compare_identical_band_is_correct():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
assert result.band == "correct"


def test_compare_identical_no_incorrect_points():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
assert result.incorrect_points == []


def test_compare_returns_compare_result():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
assert isinstance(result, CompareResult)


def test_compare_result_accuracy_in_range():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
assert 0.0 <= result.accuracy <= 1.0


def test_compare_wrong_reference_shape_raises():
bad_ref = np.zeros((10, 3), dtype=np.float32)
with pytest.raises(ValueError, match="reference must be"):
compare_gesture(GESTURES["hello"].tolist(), bad_ref)


def test_compare_very_different_gesture_low_accuracy():
ref = GESTURES["hello"]
user = GESTURES["sorry"]
result = compare_gesture(user.tolist(), ref, allow_mirror=False)
assert result.accuracy < 0.95


def test_compare_different_gestures_have_incorrect_points():
ref = GESTURES["hello"]
user = GESTURES["sorry"]
result = compare_gesture(user.tolist(), ref, allow_mirror=False)
assert len(result.incorrect_points) > 0


def test_compare_to_dict_has_expected_keys():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
d = result.to_dict()
assert set(d.keys()) == {"accuracy", "band", "incorrect_points"}


def test_compare_to_dict_accuracy_rounded():
ref = GESTURES["hello"]
result = compare_gesture(ref.tolist(), ref)
d = result.to_dict()
assert isinstance(d["accuracy"], float)


def test_compare_mirrored_input_matches_with_mirror_enabled():
ref = GESTURES["hello"]
flipped = ref.copy()
flipped[:, 0] *= -1.0
result = compare_gesture(flipped.tolist(), ref, allow_mirror=True)
assert result.accuracy > 0.9


def test_compare_mirrored_input_lower_without_mirror():
ref = GESTURES["hello"]
flipped = ref.copy()
flipped[:, 0] *= -1.0
with_mirror = compare_gesture(flipped.tolist(), ref, allow_mirror=True)
without_mirror = compare_gesture(flipped.tolist(), ref, allow_mirror=False)
assert with_mirror.accuracy >= without_mirror.accuracy


def test_compare_accepts_numpy_array_input():
ref = GESTURES["hello"]
result = compare_gesture(ref, ref)
assert result.accuracy > 0.9


def test_compare_incorrect_points_are_valid_landmark_indices():
ref = GESTURES["hello"]
user = GESTURES["sorry"]
result = compare_gesture(user.tolist(), ref, allow_mirror=False)
for idx in result.incorrect_points:
assert 0 <= idx <= 20
82 changes: 82 additions & 0 deletions tests/test_samples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import numpy as np
import pytest

from vision.core.utils import LANDMARK_COUNT
from vision.gestures.samples import GESTURES, get_reference


# ── get_reference ─────────────────────────────────────────────────────────────

def test_get_reference_returns_numpy_array():
ref = get_reference("hello")
assert isinstance(ref, np.ndarray)


def test_get_reference_correct_shape():
ref = get_reference("hello")
assert ref.shape == (LANDMARK_COUNT, 3)


def test_get_reference_dtype_is_float32():
ref = get_reference("hello")
assert ref.dtype == np.float32


def test_get_reference_unknown_raises_key_error():
with pytest.raises(KeyError, match="no reference gesture"):
get_reference("not-a-gesture")


def test_get_reference_key_error_contains_id():
with pytest.raises(KeyError, match="unknown-id"):
get_reference("unknown-id")


# ── GESTURES catalog ──────────────────────────────────────────────────────────

def test_all_gestures_have_correct_shape():
for name, arr in GESTURES.items():
assert arr.shape == (LANDMARK_COUNT, 3), f"wrong shape for '{name}'"


def test_all_gestures_dtype_is_float32():
for name, arr in GESTURES.items():
assert arr.dtype == np.float32, f"wrong dtype for '{name}'"


def test_catalog_is_nonempty():
assert len(GESTURES) > 0


def test_at_least_30_gestures_registered():
assert len(GESTURES) >= 30


def test_core_vocabulary_present():
expected = [
"hello", "thank_you", "please", "sorry", "water", "food",
"help", "stop", "yes", "no", "bathroom", "pain", "tired",
"sleep", "more", "finished",
]
for gesture_id in expected:
assert gesture_id in GESTURES, f"core gesture missing: {gesture_id}"


def test_all_26_alphabet_letters_present():
for letter in "abcdefghijklmnopqrstuvwxyz":
key = f"letter_{letter}"
assert key in GESTURES, f"missing alphabet gesture: {key}"


def test_gestures_wrist_at_origin():
for name, arr in GESTURES.items():
wrist = arr[0]
np.testing.assert_allclose(
wrist, [0.0, 0.0, 0.0], atol=1e-5,
err_msg=f"wrist of '{name}' is not at origin",
)


def test_get_reference_returns_same_object_as_gestures():
ref = get_reference("hello")
assert ref is GESTURES["hello"]
128 changes: 128 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import numpy as np
import pytest

from vision.core.utils import (
LANDMARK_COUNT,
_as_array,
mirror_horizontal,
normalize_landmarks,
to_vector,
)


def make_raw_landmarks(scale=1.0, offset=(0.0, 0.0, 0.0)):
pts = []
for i in range(LANDMARK_COUNT):
pts.append([
offset[0] + scale * (i * 0.05),
offset[1] + scale * (-(i * 0.1)),
offset[2] + scale * (i * 0.01),
])
return pts


# ── _as_array ─────────────────────────────────────────────────────────────────

def test_as_array_wrong_coord_count_raises():
with pytest.raises(ValueError):
_as_array([[0.1, 0.2]] * LANDMARK_COUNT)


def test_as_array_wrong_point_count_raises():
with pytest.raises(ValueError):
_as_array([[0.1, 0.2, 0.3]] * (LANDMARK_COUNT - 1))


def test_as_array_accepts_list_of_21():
arr = _as_array([[0.1, 0.2, 0.3]] * LANDMARK_COUNT)
assert arr.shape == (LANDMARK_COUNT, 3)


def test_as_array_dtype_is_float32():
arr = _as_array([[0.1, 0.2, 0.3]] * LANDMARK_COUNT)
assert arr.dtype == np.float32


# ── normalize_landmarks ───────────────────────────────────────────────────────

def test_normalize_wrist_lands_at_origin():
pts = make_raw_landmarks(offset=(5.0, 3.0, 1.0))
result = normalize_landmarks(pts)
np.testing.assert_allclose(result[0], [0.0, 0.0, 0.0], atol=1e-5)


def test_normalize_output_shape():
result = normalize_landmarks(make_raw_landmarks())
assert result.shape == (LANDMARK_COUNT, 3)


def test_normalize_accepts_numpy_array():
arr = np.array(make_raw_landmarks(), dtype=np.float32)
result = normalize_landmarks(arr)
assert result.shape == (LANDMARK_COUNT, 3)


def test_normalize_wrong_shape_raises():
with pytest.raises(ValueError):
normalize_landmarks([[0.1, 0.2, 0.3]] * 10)


def test_normalize_translation_invariant():
pts_a = make_raw_landmarks(offset=(0.0, 0.0, 0.0))
pts_b = make_raw_landmarks(offset=(100.0, 50.0, 25.0))
na = normalize_landmarks(pts_a)
nb = normalize_landmarks(pts_b)
np.testing.assert_allclose(na, nb, atol=1e-4)


# ── to_vector ─────────────────────────────────────────────────────────────────

def test_to_vector_output_shape():
pts = normalize_landmarks(make_raw_landmarks())
v = to_vector(pts)
assert v.shape == (63,)


def test_to_vector_dtype_is_float32():
pts = normalize_landmarks(make_raw_landmarks())
v = to_vector(pts)
assert v.dtype == np.float32


def test_to_vector_is_flattened():
pts = normalize_landmarks(make_raw_landmarks())
v = to_vector(pts)
assert v.ndim == 1


# ── mirror_horizontal ─────────────────────────────────────────────────────────

def test_mirror_flips_x_coordinates():
pts = np.array(make_raw_landmarks(), dtype=np.float32)
mirrored = mirror_horizontal(pts)
np.testing.assert_allclose(mirrored[:, 0], -pts[:, 0], atol=1e-6)


def test_mirror_preserves_y_coordinates():
pts = np.array(make_raw_landmarks(), dtype=np.float32)
mirrored = mirror_horizontal(pts)
np.testing.assert_allclose(mirrored[:, 1], pts[:, 1], atol=1e-6)


def test_mirror_preserves_z_coordinates():
pts = np.array(make_raw_landmarks(), dtype=np.float32)
mirrored = mirror_horizontal(pts)
np.testing.assert_allclose(mirrored[:, 2], pts[:, 2], atol=1e-6)


def test_mirror_does_not_mutate_original():
pts = np.array(make_raw_landmarks(), dtype=np.float32)
original_x = pts[:, 0].copy()
mirror_horizontal(pts)
np.testing.assert_allclose(pts[:, 0], original_x)


def test_mirror_twice_returns_original():
pts = np.array(make_raw_landmarks(), dtype=np.float32)
double_mirrored = mirror_horizontal(mirror_horizontal(pts))
np.testing.assert_allclose(double_mirrored, pts, atol=1e-6)
Loading