Skip to content

Commit 72a790d

Browse files
authored
Create test_signal_health.py
1 parent 1ea86db commit 72a790d

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

tests/test_signal_health.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
IX-HapticSight — Tests for interface signal health and freshness models.
3+
4+
These tests verify the backend-agnostic signal metadata layer that sits between
5+
raw sensing and runtime safety/coordination logic.
6+
"""
7+
8+
import os
9+
import sys
10+
11+
# Make project packages importable without packaging/install
12+
sys.path.insert(0, os.path.abspath("src"))
13+
14+
from ohip_interfaces.signal_health import ( # noqa: E402
15+
FreshnessPolicy,
16+
MultiSignalFreshness,
17+
SignalHealth,
18+
SignalQuality,
19+
SignalSourceMode,
20+
)
21+
22+
23+
def test_freshness_policy_respects_age_threshold():
24+
policy = FreshnessPolicy(max_age_ms=250, required=True)
25+
26+
assert policy.is_fresh(sample_timestamp_utc_s=100.0, now_utc_s=100.20) is True
27+
assert policy.is_fresh(sample_timestamp_utc_s=100.0, now_utc_s=100.251) is False
28+
29+
30+
def test_signal_quality_age_and_transport_latency_are_non_negative():
31+
quality = SignalQuality(
32+
source_mode=SignalSourceMode.LIVE,
33+
health=SignalHealth.NOMINAL,
34+
sample_timestamp_utc_s=100.0,
35+
received_timestamp_utc_s=100.015,
36+
sequence_id=12,
37+
source_name="wrist_ft",
38+
frame="tool0",
39+
)
40+
41+
assert quality.age_ms(now_utc_s=100.100) == 100.0
42+
assert quality.transport_latency_ms() == 15.0
43+
44+
45+
def test_signal_quality_is_usable_requires_health_and_freshness_when_required():
46+
policy = FreshnessPolicy(max_age_ms=200, required=True)
47+
48+
nominal_fresh = SignalQuality(
49+
source_mode=SignalSourceMode.LIVE,
50+
health=SignalHealth.NOMINAL,
51+
sample_timestamp_utc_s=50.0,
52+
received_timestamp_utc_s=50.005,
53+
)
54+
assert nominal_fresh.is_fresh(policy, now_utc_s=50.150) is True
55+
assert nominal_fresh.is_usable(policy, now_utc_s=50.150) is True
56+
57+
nominal_stale = SignalQuality(
58+
source_mode=SignalSourceMode.LIVE,
59+
health=SignalHealth.NOMINAL,
60+
sample_timestamp_utc_s=50.0,
61+
received_timestamp_utc_s=50.005,
62+
)
63+
assert nominal_stale.is_fresh(policy, now_utc_s=50.2501) is False
64+
assert nominal_stale.is_usable(policy, now_utc_s=50.2501) is False
65+
66+
degraded_fresh = SignalQuality(
67+
source_mode=SignalSourceMode.SIMULATION,
68+
health=SignalHealth.DEGRADED,
69+
sample_timestamp_utc_s=10.0,
70+
received_timestamp_utc_s=10.001,
71+
)
72+
assert degraded_fresh.is_usable(policy, now_utc_s=10.050) is True
73+
74+
invalid_signal = SignalQuality(
75+
source_mode=SignalSourceMode.LIVE,
76+
health=SignalHealth.INVALID,
77+
sample_timestamp_utc_s=10.0,
78+
received_timestamp_utc_s=10.001,
79+
)
80+
assert invalid_signal.is_usable(policy, now_utc_s=10.050) is False
81+
82+
unavailable_signal = SignalQuality(
83+
source_mode=SignalSourceMode.LIVE,
84+
health=SignalHealth.UNAVAILABLE,
85+
sample_timestamp_utc_s=10.0,
86+
received_timestamp_utc_s=10.001,
87+
)
88+
assert unavailable_signal.is_usable(policy, now_utc_s=10.050) is False
89+
90+
91+
def test_signal_quality_optional_policy_allows_stale_but_not_invalid():
92+
optional_policy = FreshnessPolicy(max_age_ms=100, required=False)
93+
94+
stale_but_nominal = SignalQuality(
95+
source_mode=SignalSourceMode.REPLAY,
96+
health=SignalHealth.NOMINAL,
97+
sample_timestamp_utc_s=20.0,
98+
received_timestamp_utc_s=20.002,
99+
)
100+
assert stale_but_nominal.is_fresh(optional_policy, now_utc_s=20.250) is False
101+
assert stale_but_nominal.is_usable(optional_policy, now_utc_s=20.250) is True
102+
103+
stale_and_invalid = SignalQuality(
104+
source_mode=SignalSourceMode.REPLAY,
105+
health=SignalHealth.INVALID,
106+
sample_timestamp_utc_s=20.0,
107+
received_timestamp_utc_s=20.002,
108+
)
109+
assert stale_and_invalid.is_usable(optional_policy, now_utc_s=20.250) is False
110+
111+
112+
def test_signal_quality_freshness_summary_contains_expected_fields():
113+
policy = FreshnessPolicy(max_age_ms=150, required=True)
114+
quality = SignalQuality(
115+
source_mode=SignalSourceMode.BENCHMARK,
116+
health=SignalHealth.DEGRADED,
117+
sample_timestamp_utc_s=75.0,
118+
received_timestamp_utc_s=75.010,
119+
sequence_id=88,
120+
source_name="sim_proximity",
121+
frame="W",
122+
note="test run",
123+
)
124+
125+
summary = quality.freshness_summary(policy, now_utc_s=75.100)
126+
127+
assert summary["source_mode"] == "BENCHMARK"
128+
assert summary["health"] == "DEGRADED"
129+
assert summary["age_ms"] == 100.0
130+
assert summary["max_age_ms"] == 150
131+
assert summary["required"] is True
132+
assert summary["fresh"] is True
133+
assert summary["usable"] is True
134+
assert summary["sequence_id"] == 88
135+
assert summary["source_name"] == "sim_proximity"
136+
assert summary["frame"] == "W"
137+
assert summary["note"] == "test run"
138+
139+
140+
def test_multi_signal_freshness_all_required_and_any_available():
141+
freshness = MultiSignalFreshness(
142+
force_torque=True,
143+
tactile=False,
144+
proximity=True,
145+
thermal=True,
146+
scene=False,
147+
)
148+
149+
assert freshness.any_available() is True
150+
151+
assert freshness.all_required(
152+
require_force_torque=True,
153+
require_proximity=True,
154+
require_thermal=True,
155+
) is True
156+
157+
assert freshness.all_required(
158+
require_force_torque=True,
159+
require_tactile=True,
160+
) is False
161+
162+
none_available = MultiSignalFreshness()
163+
assert none_available.any_available() is False

0 commit comments

Comments
 (0)