Skip to content

Commit 2bd62d4

Browse files
authored
tests: add schema round-trips, TTL/expiry checks, and utility clamps (v0.1)
1 parent d07235a commit 2bd62d4

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

tests/test_schemas.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
IX-HapticSight — Tests for canonical schemas and helpers.
3+
4+
These tests intentionally avoid external deps and package setup by
5+
inserting the project's `src/` into sys.path and importing `ohip.*`.
6+
"""
7+
8+
import os
9+
import sys
10+
from time import time
11+
from datetime import datetime, timezone
12+
13+
# Make `ohip` importable without packaging
14+
sys.path.insert(0, os.path.abspath("src"))
15+
16+
from ohip.schemas import ( # noqa: E402
17+
SafetyLevel, HazardClass, ConsentMode, ConsentSource, NudgeLevel,
18+
Vector3, RPY, Pose,
19+
ConsentRecord, SafetyMapCell, Nudge,
20+
ImpedanceProfile, ContactPlan, RestTargets,
21+
clamp, validate_priority
22+
)
23+
24+
25+
def test_consent_record_ttl_active_and_expired():
26+
now_ts = time()
27+
ts_iso = datetime.fromtimestamp(now_ts, timezone.utc).isoformat()
28+
29+
rec = ConsentRecord(
30+
subject_id="anon",
31+
mode=ConsentMode.EXPLICIT,
32+
source=ConsentSource.VERBAL,
33+
timestamp=ts_iso,
34+
scope=["shoulder_contact"],
35+
ttl_s=1,
36+
)
37+
assert rec.is_active(now_ts) is True
38+
# 2 seconds later -> expired
39+
assert rec.is_active(now_ts + 2.0) is False
40+
41+
42+
def test_safety_map_cell_roundtrip():
43+
cell = SafetyMapCell(cell=(1, 2, 3),
44+
hazard_class=HazardClass.HOT,
45+
level=SafetyLevel.YELLOW,
46+
updated_ms=42)
47+
d = cell.to_dict()
48+
assert d["cell"] == [1, 2, 3]
49+
assert d["class"] == "hot"
50+
assert d["level"] == "YELLOW"
51+
52+
back = SafetyMapCell.from_dict(d)
53+
assert back.cell == (1, 2, 3)
54+
assert back.hazard_class == HazardClass.HOT
55+
assert back.level == SafetyLevel.YELLOW
56+
assert back.updated_ms == 42
57+
58+
59+
def test_nudge_roundtrip_and_expiry():
60+
pose = Pose(frame="W", xyz=Vector3(0.1, 0.2, 0.3), rpy=RPY(0.0, 0.0, 1.57))
61+
n = Nudge(
62+
level=NudgeLevel.GREEN,
63+
target=pose,
64+
normal=Vector3(0.0, 0.8, 0.6),
65+
rationale="test",
66+
priority=0.8,
67+
expires_in_ms=100,
68+
)
69+
d = n.to_dict()
70+
m = Nudge.from_dict(d)
71+
assert m.level == NudgeLevel.GREEN
72+
assert m.target.frame == "W"
73+
assert m.normal.y == 0.8
74+
75+
# Expiry check: emitted at t=1.000 s, now=1.200 s -> expired (100 ms TTL)
76+
assert m.is_expired(now_utc_ms=1.2, emitted_ms=1000) is True
77+
# Not expired at 1.050 s
78+
assert m.is_expired(now_utc_ms=1.05, emitted_ms=1000) is False
79+
80+
81+
def test_contact_plan_validate_defaults():
82+
pose = Pose(frame="W", xyz=Vector3(0.4, -0.1, 1.3), rpy=RPY(0.0, 0.0, 1.57))
83+
plan = ContactPlan.from_dict({
84+
"target": pose.to_dict(),
85+
"normal": [0.0, 0.8, 0.6],
86+
"peak_force_N": 1.0,
87+
"dwell_ms": 1500,
88+
"approach_speed_mps": 0.15,
89+
"release_speed_mps": 0.20,
90+
"impedance": {
91+
"normal_N_per_mm": [0.3, 0.6],
92+
"tangential_N_per_mm": [0.1, 0.3],
93+
},
94+
"rationale": "unit-test",
95+
"consent_mode": "explicit",
96+
})
97+
# validate() is called in from_dict(), but call again to ensure idempotence
98+
plan.validate()
99+
assert plan.peak_force_N <= 1.2
100+
assert 1000 <= plan.dwell_ms <= 3000
101+
102+
103+
def test_rest_targets_to_dict_structure():
104+
rt = RestTargets()
105+
d = rt.to_dict()
106+
assert d["frame"] == "B"
107+
for key in ("index_tip", "middle_tip", "ring_tip", "little_tip"):
108+
assert key in d and len(d[key]) == 3
109+
assert all(isinstance(v, (int, float)) for v in d[key])
110+
111+
112+
def test_utils_clamp_and_priority():
113+
assert clamp(2.0, 0.0, 1.0) == 1.0
114+
assert clamp(-1.0, 0.0, 1.0) == 0.0
115+
assert validate_priority(-0.5) == 0.0
116+
assert validate_priority(1.5) == 1.0

0 commit comments

Comments
 (0)