Skip to content

Commit ddb8a6b

Browse files
cartertinneyCopilot
andcommitted
tests: preserve timing semantics in hardened tests
Retain not-before and negative observation assertions where elapsed time is part of the behavior. Use explicit runner barriers to prove handler removal occurs while inbox work is still pending. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7a85f58 commit ddb8a6b

3 files changed

Lines changed: 100 additions & 12 deletions

File tree

‎tests/unit/common/test_alarm.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def test_fn_called_w_args(self, mocker, desired_function, args, kwargs):
4141
a = Alarm(alarm_time=alarm_time, function=desired_function, args=args, kwargs=kwargs)
4242
a.start()
4343

44+
assert desired_function.call_count == 0
45+
assert not a.finished.wait(timeout=1)
4446
assert desired_function.call_count == 0
4547
assert a.finished.wait(timeout=5)
4648
assert time.time() >= alarm_time
@@ -76,6 +78,8 @@ def test_cancel_alarm(self, mocker, desired_function):
7678
a = Alarm(alarm_time=alarm_time, function=desired_function)
7779
a.start()
7880

81+
assert desired_function.call_count == 0
82+
assert not a.finished.wait(timeout=1)
7983
assert desired_function.call_count == 0
8084
a.cancel() # cancel the alarm
8185
a.join(timeout=5)

‎tests/unit/iothub/aio/test_async_handler_manager.py‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
# --------------------------------------------------------------------------
66
import logging
77
import pytest
8+
import asyncio
89
import threading
910
import concurrent.futures
1011
from azure.iot.device.common import handle_exceptions
1112
from azure.iot.device.iothub import client_event
1213
from azure.iot.device.iothub.aio.async_handler_manager import AsyncHandlerManager
13-
from azure.iot.device.iothub.sync_handler_manager import HandlerManagerException
14+
from azure.iot.device.iothub.sync_handler_manager import (
15+
HandlerManagerException,
16+
HandlerRunnerKillerSentinel,
17+
)
1418
from azure.iot.device.iothub.sync_handler_manager import MESSAGE, METHOD, TWIN_DP_PATCH
1519
from azure.iot.device.iothub.inbox_manager import InboxManager
1620
from azure.iot.device.iothub.aio.async_inbox import AsyncClientInbox
@@ -451,8 +455,31 @@ async def test_handler_resolve_pending_items_before_handler_removal(
451455
handler,
452456
handler_checker,
453457
inbox,
454-
async_wait_for,
455458
):
459+
original_get = inbox.get
460+
original_put = inbox.put
461+
runner_paused = threading.Event()
462+
resume_runner = threading.Event()
463+
sentinel_added = threading.Event()
464+
get_count = 0
465+
466+
async def controlled_get():
467+
nonlocal get_count
468+
item = await original_get()
469+
get_count += 1
470+
if get_count == 2:
471+
runner_paused.set()
472+
assert await asyncio.to_thread(resume_runner.wait, 5)
473+
return item
474+
475+
def tracked_put(item):
476+
original_put(item)
477+
if isinstance(item, HandlerRunnerKillerSentinel):
478+
sentinel_added.set()
479+
480+
mocker.patch.object(inbox, "get", side_effect=controlled_get)
481+
mocker.patch.object(inbox, "put", side_effect=tracked_put)
482+
456483
assert inbox.empty()
457484
# Queue up a bunch of items in the inbox
458485
for _ in range(100):
@@ -463,9 +490,22 @@ async def test_handler_resolve_pending_items_before_handler_removal(
463490
assert not inbox.empty()
464491
# Set the handler
465492
setattr(handler_manager, handler_name, handler)
466-
# Immediately remove the handler
467-
setattr(handler_manager, handler_name, None)
468-
await async_wait_for(lambda: handler_checker.handler_call_count >= 100)
493+
try:
494+
assert runner_paused.wait(timeout=5)
495+
assert handler_checker.handler_call_count < 100
496+
assert not inbox.empty()
497+
498+
# Remove the handler while items are still pending
499+
loop = asyncio.get_running_loop()
500+
removal_future = loop.run_in_executor(
501+
None, setattr, handler_manager, handler_name, None
502+
)
503+
assert sentinel_added.wait(timeout=5)
504+
assert not removal_future.done()
505+
finally:
506+
resume_runner.set()
507+
await asyncio.wait_for(removal_future, timeout=5)
508+
469509
# Despite removal, handler has been called for everything that was in the inbox at the
470510
# time of the removal
471511
assert handler_checker.handler_call_count == 100
@@ -474,6 +514,8 @@ async def test_handler_resolve_pending_items_before_handler_removal(
474514
# Add some more items
475515
for _ in range(100):
476516
inbox.put(mocker.MagicMock())
517+
# Give any incorrectly retained runner an opportunity to invoke the handler
518+
await asyncio.sleep(0.1)
477519
# Despite more items added to inbox, no further handler calls have been made beyond the
478520
# initial calls that were made when the original items were added
479521
assert handler_checker.handler_call_count == 100

‎tests/unit/iothub/test_sync_handler_manager.py‎

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
import logging
77
import pytest
88
import threading
9+
import time
10+
import concurrent.futures
911
from azure.iot.device.common import handle_exceptions
1012
from azure.iot.device.iothub import client_event
11-
from azure.iot.device.iothub.sync_handler_manager import SyncHandlerManager, HandlerManagerException
13+
from azure.iot.device.iothub.sync_handler_manager import (
14+
SyncHandlerManager,
15+
HandlerManagerException,
16+
HandlerRunnerKillerSentinel,
17+
)
1218
from azure.iot.device.iothub.sync_handler_manager import MESSAGE, METHOD, TWIN_DP_PATCH
1319
from azure.iot.device.iothub.inbox_manager import InboxManager
1420
from azure.iot.device.iothub.sync_inbox import SyncClientInbox
@@ -387,6 +393,30 @@ def test_handler_invoked_multiple(self, mocker, handler_name, handler_manager, i
387393
def test_handler_resolve_pending_items_before_handler_removal(
388394
self, mocker, handler_name, handler_manager, inbox
389395
):
396+
original_get = inbox.get
397+
original_put = inbox.put
398+
runner_paused = threading.Event()
399+
resume_runner = threading.Event()
400+
sentinel_added = threading.Event()
401+
get_count = 0
402+
403+
def controlled_get(*args, **kwargs):
404+
nonlocal get_count
405+
item = original_get(*args, **kwargs)
406+
get_count += 1
407+
if get_count == 2:
408+
runner_paused.set()
409+
assert resume_runner.wait(timeout=5)
410+
return item
411+
412+
def tracked_put(item):
413+
original_put(item)
414+
if isinstance(item, HandlerRunnerKillerSentinel):
415+
sentinel_added.set()
416+
417+
mocker.patch.object(inbox, "get", side_effect=controlled_get)
418+
mocker.patch.object(inbox, "put", side_effect=tracked_put)
419+
390420
# Use a threadsafe mock to ensure accurate counts
391421
mock_handler = ThreadsafeMock()
392422
assert inbox.empty()
@@ -399,13 +429,23 @@ def test_handler_resolve_pending_items_before_handler_removal(
399429
assert not inbox.empty()
400430
# Set the handler
401431
setattr(handler_manager, handler_name, mock_handler)
402-
# The handler has not yet been called for everything that was in the inbox
403-
# NOTE: I'd really like to show that the handler call count is also > 0 here, but
404-
# it's pretty difficult to make the timing work
405-
assert mock_handler.call_count < 100
432+
try:
433+
assert runner_paused.wait(timeout=5)
434+
assert mock_handler.call_count < 100
435+
assert not inbox.empty()
436+
437+
# Remove the handler while items are still pending
438+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
439+
removal_future = executor.submit(setattr, handler_manager, handler_name, None)
440+
try:
441+
assert sentinel_added.wait(timeout=5)
442+
assert not removal_future.done()
443+
finally:
444+
resume_runner.set()
445+
removal_future.result(timeout=5)
446+
finally:
447+
resume_runner.set()
406448

407-
# Immediately remove the handler
408-
setattr(handler_manager, handler_name, None)
409449
# Despite removal, handler has been called for everything that was in the inbox at the
410450
# time of the removal
411451
assert mock_handler.call_count == 100
@@ -414,6 +454,8 @@ def test_handler_resolve_pending_items_before_handler_removal(
414454
# Add some more items
415455
for _ in range(100):
416456
inbox.put(mocker.MagicMock())
457+
# Give any incorrectly retained runner an opportunity to invoke the handler
458+
time.sleep(0.2)
417459
# Despite more items added to inbox, no further handler calls have been made beyond the
418460
# initial calls that were made when the original items were added
419461
assert mock_handler.call_count == 100

0 commit comments

Comments
 (0)