@@ -367,6 +367,113 @@ def fake_stream(func, **kw):
367367 self .assertEqual (len (cached ), 1 )
368368 self .assertIs (cached [0 ], pod )
369369
370+ def test_bookmark_advances_resource_version (self ):
371+ """A BOOKMARK event causes the informer's _resource_version to advance.
372+
373+ PR #2505 added BOOKMARK-aware handling to Watch.unmarshal_event: it
374+ extracts resourceVersion from the raw BOOKMARK dict and stores it on
375+ self.resource_version *without* deserialising the object (because
376+ BOOKMARK events may be incomplete). The informer must read that value
377+ back so that the next watch reconnect starts from the BOOKMARK's RV
378+ rather than the initial-list RV.
379+ """
380+ bookmark_obj = {"metadata" : {"resourceVersion" : "100" }}
381+
382+ list_func = MagicMock ()
383+ list_resp = MagicMock ()
384+ list_resp .items = []
385+ list_resp .metadata = MagicMock (resource_version = "5" )
386+ list_func .return_value = list_resp
387+
388+ informer = SharedInformer (list_func = list_func )
389+
390+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
391+ mock_w = MagicMock ()
392+ # Simulate Watch.unmarshal_event updating resource_version on BOOKMARK.
393+ mock_w .resource_version = "100"
394+
395+ def fake_stream (func , ** kw ):
396+ yield {"type" : "BOOKMARK" , "object" : bookmark_obj , "raw_object" : bookmark_obj }
397+ informer ._stop_event .set ()
398+
399+ mock_w .stream .side_effect = fake_stream
400+ MockWatch .return_value = mock_w
401+
402+ informer .start ()
403+ informer ._thread .join (timeout = 3 )
404+
405+ # The informer must have synced the RV from the BOOKMARK.
406+ self .assertEqual (informer ._resource_version , "100" )
407+
408+ def test_bookmark_handler_receives_raw_dict (self ):
409+ """BOOKMARK handlers receive the raw dict, not a deserialized model.
410+
411+ Watch intentionally skips deserialization for BOOKMARK events (PR #2505)
412+ because BOOKMARK objects may be incomplete. The informer passes
413+ ``event.get('raw_object', obj)`` to the BOOKMARK handler, so it must
414+ always be a dict rather than a typed Kubernetes model object.
415+ """
416+ bookmark_obj = {"metadata" : {"resourceVersion" : "77" }}
417+ received = []
418+
419+ list_func = MagicMock ()
420+ list_resp = MagicMock ()
421+ list_resp .items = []
422+ list_resp .metadata = MagicMock (resource_version = "1" )
423+ list_func .return_value = list_resp
424+
425+ informer = SharedInformer (list_func = list_func )
426+ informer .add_event_handler (BOOKMARK , received .append )
427+
428+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
429+ mock_w = MagicMock ()
430+ mock_w .resource_version = "77"
431+
432+ def fake_stream (func , ** kw ):
433+ yield {"type" : "BOOKMARK" , "object" : bookmark_obj , "raw_object" : bookmark_obj }
434+ informer ._stop_event .set ()
435+
436+ mock_w .stream .side_effect = fake_stream
437+ MockWatch .return_value = mock_w
438+
439+ informer .start ()
440+ informer ._thread .join (timeout = 3 )
441+
442+ self .assertEqual (len (received ), 1 )
443+ # Must be the raw dict, not a deserialized model.
444+ self .assertIsInstance (received [0 ], dict )
445+ self .assertEqual (received [0 ]["metadata" ]["resourceVersion" ], "77" )
446+
447+ def test_multiple_bookmarks_advance_resource_version_to_latest (self ):
448+ """Multiple BOOKMARK events each update _resource_version to the latest value."""
449+ list_func = MagicMock ()
450+ list_resp = MagicMock ()
451+ list_resp .items = []
452+ list_resp .metadata = MagicMock (resource_version = "1" )
453+ list_func .return_value = list_resp
454+
455+ informer = SharedInformer (list_func = list_func )
456+
457+ rv_sequence = iter (["10" , "20" , "30" ])
458+
459+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
460+ mock_w = MagicMock ()
461+
462+ def fake_stream (func , ** kw ):
463+ for rv in ["10" , "20" , "30" ]:
464+ bk = {"metadata" : {"resourceVersion" : rv }}
465+ mock_w .resource_version = rv
466+ yield {"type" : "BOOKMARK" , "object" : bk , "raw_object" : bk }
467+ informer ._stop_event .set ()
468+
469+ mock_w .stream .side_effect = fake_stream
470+ MockWatch .return_value = mock_w
471+
472+ informer .start ()
473+ informer ._thread .join (timeout = 3 )
474+
475+ self .assertEqual (informer ._resource_version , "30" )
476+
370477 def test_resync_period_triggers_full_list (self ):
371478 """A full List call must be made to the API server on every resync_period.
372479
0 commit comments