@@ -114,6 +114,14 @@ def worker(n):
114114 for t in threads :
115115 t .join ()
116116 self .assertEqual (errors , [])
117+ # Verify that the cache actually holds the objects that were put into it.
118+ for n in range (5 ):
119+ for i in range (50 ):
120+ key = "default/pod-{}-{}" .format (n , i )
121+ self .assertIsNotNone (
122+ self .cache .get_by_key (key ),
123+ "expected key {} in cache" .format (key ),
124+ )
117125
118126
119127class TestSharedInformerHandlers (unittest .TestCase ):
@@ -360,7 +368,13 @@ def fake_stream(func, **kw):
360368 self .assertIs (cached [0 ], pod )
361369
362370 def test_resync_period_triggers_full_list (self ):
363- """A full List call must be made to the API server on every resync_period."""
371+ """A full List call must be made to the API server on every resync_period.
372+
373+ With the new implementation the watch stream is given a server-side
374+ timeout equal to resync_period (via timeout_seconds). When the stream
375+ exits, the elapsed-time check fires the resync even if no events
376+ arrived – this is exactly the scenario this test exercises.
377+ """
364378 pod = _make_pod ("default" , "resync-pod" )
365379
366380 list_func = MagicMock ()
@@ -371,20 +385,31 @@ def test_resync_period_triggers_full_list(self):
371385
372386 informer = SharedInformer (list_func = list_func , resync_period = 60 )
373387
388+ stream_calls = {"n" : 0 }
389+
374390 with patch ("kubernetes.informer.informer.Watch" ) as MockWatch , \
375391 patch ("kubernetes.informer.informer.time" ) as mock_time :
376- # Sequence of time.monotonic() calls:
377- # 1. last_resync = time.monotonic() → 0.0
378- # 2. (time.monotonic() - last_resync) check → 61.0 (triggers resync)
379- # 3. last_resync = time.monotonic() → 61.0 (reset after resync)
392+ # Sequence of time.monotonic() calls inside _run_loop:
393+ # 1. last_resync = time.monotonic() → 0.0 (watch-loop start)
394+ # 2. post-stream: time.monotonic() → 61.0 (≥60 → resync fires)
395+ # 3. last_resync = time.monotonic() → 61.0 (second watch-loop start)
396+ # The stop_event is set during the second stream, so the
397+ # post-stream check is short-circuited and no further calls occur.
380398 mock_time .monotonic .side_effect = [0.0 , 61.0 , 61.0 ]
399+ mock_time .sleep = time .sleep # keep real sleep/wait working
381400
382401 mock_w = MagicMock ()
383402 mock_w .resource_version = "5"
384403
385404 def fake_stream (func , ** kw ):
386- yield {"type" : "ADDED" , "object" : pod }
405+ stream_calls ["n" ] += 1
406+ if stream_calls ["n" ] == 1 :
407+ # Simulate the stream timing out (timeout_seconds expired)
408+ # with no events – the resync should fire after this returns.
409+ return iter ([])
410+ # Second iteration: stop the informer.
387411 informer ._stop_event .set ()
412+ return iter ([])
388413
389414 mock_w .stream .side_effect = fake_stream
390415 MockWatch .return_value = mock_w
@@ -955,8 +980,12 @@ def list_func(**kw):
955980 return resp
956981
957982 modified = []
983+ added = []
984+ deleted = []
958985 informer = SharedInformer (list_func = list_func )
959986 informer .add_event_handler (MODIFIED , modified .append )
987+ informer .add_event_handler (ADDED , added .append )
988+ informer .add_event_handler (DELETED , deleted .append )
960989
961990 stream_calls = {"n" : 0 }
962991
@@ -980,6 +1009,11 @@ def fake_stream(func, **kw):
9801009 # pod was in both the initial list (call 1) and the re-list (call 2).
9811010 # On the re-list it should fire MODIFIED (not ADDED again).
9821011 self .assertIn (pod , modified )
1012+ # ADDED fires exactly once: for the initial list. The re-list must
1013+ # NOT fire a second ADDED for an already-cached item.
1014+ self .assertEqual (len (added ), 1 , "ADDED should fire once (initial list) but not again on re-list" )
1015+ # DELETED must not fire for an item present in both lists.
1016+ self .assertEqual (deleted , [], "DELETED should not fire for an item present in both lists" )
9831017 # Still in cache.
9841018 self .assertIsNotNone (informer .cache .get_by_key ("default/stable-pod" ))
9851019
0 commit comments