Skip to content

Commit 430b843

Browse files
committed
Improved speed of tests down to ~145 seconds
1 parent bd3566f commit 430b843

10 files changed

Lines changed: 99 additions & 48 deletions

File tree

modules/01-operating-room/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def proc_manager_secure(dds_env_secure):
8787
pm.shutdown_all()
8888

8989

90-
@pytest.fixture()
90+
@pytest.fixture(scope="session")
9191
def dds_participant(dds_env):
9292
"""Create a lightweight DDS DomainParticipant for test observation."""
9393
import rti.connextdds as dds

modules/01-operating-room/tests/module01_test_support.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ def shutdown_all(self):
135135
os.killpg(p.pid, signal.SIGTERM)
136136
except (ProcessLookupError, PermissionError):
137137
p.terminate()
138-
# Give processes 3 seconds to exit gracefully
139-
deadline = time.monotonic() + 3
138+
# Give processes 1 second to exit gracefully
139+
deadline = time.monotonic() + 1
140140
for p in self._children:
141141
remaining = max(0, deadline - time.monotonic())
142142
try:
@@ -272,18 +272,35 @@ def wait_for_data(reader, timeout_sec: float = 5.0, min_count: int = 1):
272272

273273

274274
def wait_for_process_ready(proc, timeout_sec: float = 5.0):
275-
"""Wait until *proc* survives for *timeout_sec* or exits early.
275+
"""Wait until *proc* produces stdout output, exits, or *timeout_sec* expires.
276276
277-
Polls ``proc.poll()`` every 250ms. Returns as soon as either:
277+
Returns as soon as any of these conditions is met:
278+
- The process writes to stdout (indicates successful startup).
278279
- The process exits (caller should check ``proc.returncode``).
279-
- The full *timeout_sec* elapses with the process still running (success
280-
for smoke tests — the process survived its startup window).
280+
- The full *timeout_sec* elapses with the process still running.
281281
"""
282-
deadline = time.monotonic() + timeout_sec
283-
while time.monotonic() < deadline:
284-
if proc.poll() is not None:
285-
return
286-
time.sleep(0.25)
282+
import selectors
283+
284+
if proc.poll() is not None:
285+
return
286+
287+
sel = selectors.DefaultSelector()
288+
try:
289+
if proc.stdout and hasattr(proc.stdout, "fileno"):
290+
sel.register(proc.stdout, selectors.EVENT_READ)
291+
deadline = time.monotonic() + timeout_sec
292+
while time.monotonic() < deadline:
293+
if proc.poll() is not None:
294+
return
295+
remaining = max(0, deadline - time.monotonic())
296+
if sel.get_map():
297+
events = sel.select(timeout=min(remaining, 0.25))
298+
if events:
299+
return # stdout has data — process started successfully
300+
else:
301+
time.sleep(min(remaining, 0.25))
302+
finally:
303+
sel.close()
287304

288305

289306
def wait_for_device_status(

modules/01-operating-room/tests/test_dds_communication.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def test_arm_receives_motor_control(self, proc_manager, dds_participant):
249249
direction=SurgicalRobot.MotorDirections.INCREMENT,
250250
)
251251
writer.write(cmd)
252-
time.sleep(1)
252+
time.sleep(0.5)
253253

254254
assert proc.poll() is None, "Arm crashed after receiving MotorControl"
255255

@@ -419,7 +419,7 @@ def test_patient_sensor_ignores_arm_command(self, proc_manager, dds_participant)
419419

420420
# Give time for any reaction and drain vitals
421421
vitals_reader.take()
422-
time.sleep(1.5)
422+
time.sleep(0.5)
423423

424424
# PatientSensor should still be publishing vitals (not paused)
425425
fresh = wait_for_data(vitals_reader, timeout_sec=3.0, min_count=1)

modules/01-operating-room/tests/test_demo_flow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def test_pause_stops_vitals_then_resume_restarts(self, proc_manager, dds_partici
177177

178178
# Drain any remaining vitals and then check no new ones arrive
179179
vitals_reader.take() # drain
180-
time.sleep(2)
180+
time.sleep(1)
181181
stale = vitals_reader.take()
182182
valid_stale = [s for s in stale if s.info.valid]
183183
assert len(valid_stale) <= 1, (
@@ -284,7 +284,7 @@ def test_vitals_flow_with_security(self, proc_manager_secure, dds_env_secure):
284284
ps = proc_manager_secure.start_app("PatientSensor")
285285

286286
# Wait for security handshake and DDS initialization
287-
wait_for_process_ready(ps, timeout_sec=15)
287+
wait_for_process_ready(ps, timeout_sec=5)
288288

289289
if ps.poll() is not None:
290290
stdout = ps.stdout.read().decode(errors="replace") if ps.stdout else ""

modules/02-record-playback/tests/module02_test_support.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def shutdown_all(self):
147147
os.killpg(p.pid, signal.SIGTERM)
148148
except (ProcessLookupError, PermissionError):
149149
p.terminate()
150-
deadline = time.monotonic() + 5
150+
deadline = time.monotonic() + 2
151151
for p in self._children:
152152
remaining = max(0, deadline - time.monotonic())
153153
try:
@@ -164,12 +164,29 @@ def shutdown_all(self):
164164

165165

166166
def wait_for_process_ready(proc, timeout_sec: float = 5.0):
167-
"""Wait until *proc* survives for *timeout_sec* or exits early."""
168-
deadline = time.monotonic() + timeout_sec
169-
while time.monotonic() < deadline:
170-
if proc.poll() is not None:
171-
return
172-
time.sleep(0.25)
167+
"""Wait until *proc* produces stdout output, exits, or *timeout_sec* expires."""
168+
import selectors
169+
170+
if proc.poll() is not None:
171+
return
172+
173+
sel = selectors.DefaultSelector()
174+
try:
175+
if proc.stdout and hasattr(proc.stdout, "fileno"):
176+
sel.register(proc.stdout, selectors.EVENT_READ)
177+
deadline = time.monotonic() + timeout_sec
178+
while time.monotonic() < deadline:
179+
if proc.poll() is not None:
180+
return
181+
remaining = max(0, deadline - time.monotonic())
182+
if sel.get_map():
183+
events = sel.select(timeout=min(remaining, 0.25))
184+
if events:
185+
return
186+
else:
187+
time.sleep(min(remaining, 0.25))
188+
finally:
189+
sel.close()
173190

174191

175192
RECORDING_DIR = MODULE_DIR / "or_recording"

modules/02-record-playback/tests/test_recording.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def test_recording_creates_database(self, proc_manager, clean_recording_dir):
5050
],
5151
cwd=MODULE_DIR,
5252
)
53-
# Record for ~8 seconds
54-
time.sleep(8)
53+
# Record for ~3 seconds
54+
time.sleep(3)
5555

5656
# Verify Recording Service is still alive
5757
assert rec_proc.poll() is None, (

modules/02-record-playback/tests/test_replay.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_replay_produces_vitals(self, proc_manager, dds_env_dict, clean_recordin
5757
],
5858
cwd=MODULE_DIR,
5959
)
60-
time.sleep(8)
60+
time.sleep(3)
6161

6262
rec_proc.terminate()
6363
try:

modules/04-security-threat/tests/module04_test_support.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def shutdown_all(self):
106106
os.killpg(p.pid, signal.SIGTERM)
107107
except (ProcessLookupError, PermissionError):
108108
p.terminate()
109-
deadline = time.monotonic() + 5
109+
deadline = time.monotonic() + 2
110110
for p in self._children:
111111
remaining = max(0, deadline - time.monotonic())
112112
try:
@@ -121,12 +121,29 @@ def shutdown_all(self):
121121

122122

123123
def wait_for_process_ready(proc, timeout_sec: float = 5.0):
124-
"""Wait until *proc* survives for *timeout_sec* or exits early."""
125-
deadline = time.monotonic() + timeout_sec
126-
while time.monotonic() < deadline:
127-
if proc.poll() is not None:
128-
return
129-
time.sleep(0.25)
124+
"""Wait until *proc* produces stdout output, exits, or *timeout_sec* expires."""
125+
import selectors
126+
127+
if proc.poll() is not None:
128+
return
129+
130+
sel = selectors.DefaultSelector()
131+
try:
132+
if proc.stdout and hasattr(proc.stdout, "fileno"):
133+
sel.register(proc.stdout, selectors.EVENT_READ)
134+
deadline = time.monotonic() + timeout_sec
135+
while time.monotonic() < deadline:
136+
if proc.poll() is not None:
137+
return
138+
remaining = max(0, deadline - time.monotonic())
139+
if sel.get_map():
140+
events = sel.select(timeout=min(remaining, 0.25))
141+
if events:
142+
return
143+
else:
144+
time.sleep(min(remaining, 0.25))
145+
finally:
146+
sel.close()
130147

131148

132149
# ---------------------------------------------------------------------------

modules/04-security-threat/tests/test_threat_exfiltrator.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def test_unsecure_exfiltration_succeeds(self, or_pm_nonsecure, or_env_nonsecure,
173173
_assert_unsecure_exfiltrator_probe_launch(threat_env)
174174

175175
ps = or_pm_nonsecure.start_app("PatientSensor")
176-
wait_for_process_ready(ps, timeout_sec=10)
176+
wait_for_process_ready(ps, timeout_sec=5)
177177
assert ps.poll() is None, f"PatientSensor exited early with code {ps.returncode}"
178178

179179
result = _run_exfiltrator_probe(
@@ -204,7 +204,7 @@ def test_unsecure_exfiltrator_vs_secure_or(self, or_pm_secure, or_env_secure, th
204204
_assert_unsecure_exfiltrator_probe_launch(threat_env)
205205

206206
ps = or_pm_secure.start_app("PatientSensor")
207-
wait_for_process_ready(ps, timeout_sec=15)
207+
wait_for_process_ready(ps, timeout_sec=5)
208208
assert ps.poll() is None, f"PatientSensor exited early with code {ps.returncode}"
209209

210210
result = _run_exfiltrator_probe(
@@ -222,13 +222,13 @@ def test_unsecure_exfiltrator_vs_secure_or(self, or_pm_secure, or_env_secure, th
222222
def test_rogue_ca_exfiltrator_blocked(self, or_pm_secure, or_env_secure, threat_env):
223223
"""Rogue CA exfiltrator should NOT receive vitals from secured OR apps."""
224224
ps = or_pm_secure.start_app("PatientSensor")
225-
wait_for_process_ready(ps, timeout_sec=15)
225+
wait_for_process_ready(ps, timeout_sec=5)
226226
assert ps.poll() is None, f"PatientSensor exited early with code {ps.returncode}"
227227

228228
result = _run_exfiltrator_probe(
229229
threat_env[0],
230230
dp_name="ThreatParticipantLibrary::dp/ThreatExfiltrator/RogueCA",
231-
timeout_sec=10,
231+
timeout_sec=4,
232232
)
233233
assert result["received"] == 0, (
234234
"Rogue CA exfiltrator should NOT receive vitals from secured OR"
@@ -237,13 +237,13 @@ def test_rogue_ca_exfiltrator_blocked(self, or_pm_secure, or_env_secure, threat_
237237
def test_forged_perms_exfiltrator_blocked(self, or_pm_secure, or_env_secure, threat_env):
238238
"""Forged permissions exfiltrator should NOT receive vitals from secured OR apps."""
239239
ps = or_pm_secure.start_app("PatientSensor")
240-
wait_for_process_ready(ps, timeout_sec=15)
240+
wait_for_process_ready(ps, timeout_sec=5)
241241
assert ps.poll() is None, f"PatientSensor exited early with code {ps.returncode}"
242242

243243
result = _run_exfiltrator_probe(
244244
threat_env[0],
245245
dp_name="ThreatParticipantLibrary::dp/ThreatExfiltrator/ForgedPerms",
246-
timeout_sec=10,
246+
timeout_sec=4,
247247
)
248248
assert result["received"] == 0, (
249249
"Forged permissions exfiltrator should NOT receive vitals from secured OR"
@@ -252,13 +252,13 @@ def test_forged_perms_exfiltrator_blocked(self, or_pm_secure, or_env_secure, thr
252252
def test_expired_cert_exfiltrator_blocked(self, or_pm_secure, or_env_secure, threat_env):
253253
"""Expired certificate exfiltrator should fail to create participant or receive data."""
254254
ps = or_pm_secure.start_app("PatientSensor")
255-
wait_for_process_ready(ps, timeout_sec=15)
255+
wait_for_process_ready(ps, timeout_sec=5)
256256
assert ps.poll() is None, f"PatientSensor exited early with code {ps.returncode}"
257257

258258
result = _run_exfiltrator_probe(
259259
threat_env[0],
260260
dp_name="ThreatParticipantLibrary::dp/ThreatExfiltrator/ExpiredCert",
261-
timeout_sec=10,
261+
timeout_sec=4,
262262
)
263263
# Expired cert typically causes participant creation failure
264264
if result["created"]:

modules/04-security-threat/tests/test_threat_injector.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class TestInjectorUnsecure:
118118
def test_unsecure_injection_succeeds(self, or_pm_nonsecure, or_env_nonsecure, threat_env):
119119
"""Unsecured injector should match unsecured OR apps."""
120120
sensor = or_pm_nonsecure.start_app("PatientSensor")
121-
wait_for_process_ready(sensor, timeout_sec=10)
121+
wait_for_process_ready(sensor, timeout_sec=5)
122122
assert sensor.poll() is None, f"PatientSensor exited early with code {sensor.returncode}"
123123

124124
result = _run_injector_probe(
@@ -141,37 +141,37 @@ class TestInjectorSecure:
141141
def test_rogue_ca_injection_blocked(self, or_pm_secure, or_env_secure, threat_env):
142142
"""Injector with rogue CA identity should not match secured OR apps."""
143143
ps = or_pm_secure.start_app("PatientSensor")
144-
wait_for_process_ready(ps, timeout_sec=15)
144+
wait_for_process_ready(ps, timeout_sec=5)
145145

146146
result = _run_injector_probe(
147147
threat_env[0],
148148
dp_name="ThreatParticipantLibrary::dp/ThreatInjector/RogueCA",
149-
timeout_sec=10,
149+
timeout_sec=4,
150150
)
151151
# The participant may be created but should NOT match
152152
assert not result["matched"], "Rogue CA injector should NOT match secured OR apps"
153153

154154
def test_forged_perms_injection_blocked(self, or_pm_secure, or_env_secure, threat_env):
155155
"""Injector with forged permissions should not match secured OR apps."""
156156
ps = or_pm_secure.start_app("PatientSensor")
157-
wait_for_process_ready(ps, timeout_sec=15)
157+
wait_for_process_ready(ps, timeout_sec=5)
158158

159159
result = _run_injector_probe(
160160
threat_env[0],
161161
dp_name="ThreatParticipantLibrary::dp/ThreatInjector/ForgedPerms",
162-
timeout_sec=10,
162+
timeout_sec=4,
163163
)
164164
assert not result["matched"], "Forged permissions injector should NOT match secured OR apps"
165165

166166
def test_expired_cert_injection_fails(self, or_pm_secure, or_env_secure, threat_env):
167167
"""Injector with expired certificate should fail to create participant or match."""
168168
ps = or_pm_secure.start_app("PatientSensor")
169-
wait_for_process_ready(ps, timeout_sec=15)
169+
wait_for_process_ready(ps, timeout_sec=5)
170170

171171
result = _run_injector_probe(
172172
threat_env[0],
173173
dp_name="ThreatParticipantLibrary::dp/ThreatInjector/ExpiredCert",
174-
timeout_sec=10,
174+
timeout_sec=4,
175175
)
176176
# Expired cert typically causes participant creation failure
177177
if result["created"]:

0 commit comments

Comments
 (0)