1+ import json
12import time
23from datetime import datetime
3- from typing import Tuple
4+ from typing import Callable , Tuple
45from uuid import uuid4
56
67from alitra import Frame , Orientation , Pose , Position
3132)
3233
3334
35+ def _wait_until (
36+ predicate : Callable [[], bool ], timeout : float = 5.0 , interval : float = 0.01
37+ ) -> None :
38+ deadline = time .monotonic () + timeout
39+ while time .monotonic () < deadline :
40+ if predicate ():
41+ return
42+ time .sleep (interval )
43+ raise AssertionError (f"Timed out after { timeout } s waiting for predicate" )
44+
45+
3446def test_should_upload_from_queue (
3547 container : ApplicationContainer , uploader_thread : UploaderThreadMock
3648) -> None :
@@ -65,12 +77,10 @@ def test_should_upload_from_queue(
6577 )
6678
6779 uploader : Uploader = container .uploader ()
80+ storage_handler : StorageFake = uploader .storage_handlers [0 ] # type: ignore
6881
6982 uploader .upload_queue .put (message )
70- time .sleep (0.01 )
71-
72- storage_handler : StorageFake = uploader .storage_handlers [0 ] # type: ignore
73- assert inspection in storage_handler .stored_inspections
83+ _wait_until (lambda : inspection in storage_handler .stored_inspections )
7484
7585
7686def test_should_retry_failed_upload_from_queue (
@@ -93,15 +103,15 @@ def test_should_retry_failed_upload_from_queue(
93103 # Need it to fail so that it retries
94104 storage_handler .will_fail = True
95105 uploader .upload_queue .put (message )
106+ # Fixed wait: asserting absence of upload requires a real elapsed window
96107 time .sleep (1 )
97108
98109 # Should not upload, instead raise StorageException
99110 assert not storage_handler .blob_exists (inspection )
100111 storage_handler .will_fail = False
101- time .sleep (3 )
102112
103- # After some time, it should have retried and now it should be successful
104- assert storage_handler .blob_exists (inspection )
113+ # Retry succeeds once exponential backoff elapses
114+ _wait_until ( lambda : storage_handler .blob_exists (inspection ), timeout = 5.0 )
105115
106116
107117def test_should_not_publish_when_blob_paths_are_empty (
@@ -127,8 +137,59 @@ def test_should_not_publish_when_blob_paths_are_empty(
127137 mission ,
128138 )
129139 uploader .upload_queue .put (message )
130- time .sleep (1 )
131-
132- assert inspection in storage_handler .stored
140+ _wait_until (lambda : inspection in storage_handler .stored )
133141
142+ # Brief quiet period to confirm no MQTT publish follows the store
143+ time .sleep (0.1 )
134144 assert len (mqtt_fake .published ) == 0
145+
146+
147+ def _put_inspection_with_analysis_types (
148+ container : ApplicationContainer ,
149+ analysis_types : list [str ] | None ,
150+ ) -> MqttPublisherFake :
151+ metadata = ImageMetadata (
152+ start_time = datetime .now (),
153+ robot_pose = Pose (
154+ Position (0 , 0 , 0 , Frame ("asset" )),
155+ Orientation (x = 0 , y = 0 , z = 0 , w = 1 , frame = Frame ("asset" )),
156+ Frame ("asset" ),
157+ ),
158+ target_position = Position (0 , 0 , 0 , Frame ("asset" )),
159+ file_type = "jpg" ,
160+ analysis_types = analysis_types ,
161+ )
162+ inspection = InspectionBlob (metadata = metadata , id = str (uuid4 ()))
163+ mission = Mission (name = "m" )
164+
165+ uploader : Uploader = container .uploader ()
166+ mqtt_fake = MqttPublisherFake ()
167+ uploader .mqtt_publisher = mqtt_fake
168+
169+ message : Tuple [Inspection , Mission ] = (inspection , mission )
170+ uploader .upload_queue .put (message )
171+ return mqtt_fake
172+
173+
174+ def test_publishes_required_analysis_when_present (
175+ container : ApplicationContainer , uploader_thread : UploaderThreadMock
176+ ) -> None :
177+ uploader_thread .start ()
178+ mqtt_fake = _put_inspection_with_analysis_types (
179+ container , ["anonymize" , "thermal-reading" ]
180+ )
181+ _wait_until (lambda : mqtt_fake .count () == 1 )
182+
183+ payload = json .loads (mqtt_fake .last ()["payload" ])
184+ assert payload ["required_analysis" ] == ["anonymize" , "thermal-reading" ]
185+
186+
187+ def test_publishes_null_required_analysis_when_absent (
188+ container : ApplicationContainer , uploader_thread : UploaderThreadMock
189+ ) -> None :
190+ uploader_thread .start ()
191+ mqtt_fake = _put_inspection_with_analysis_types (container , None )
192+ _wait_until (lambda : mqtt_fake .count () == 1 )
193+
194+ payload = json .loads (mqtt_fake .last ()["payload" ])
195+ assert payload ["required_analysis" ] is None
0 commit comments