2020
2121import json
2222
23- from unittest .mock import Mock , call
23+ from unittest .mock import Mock , call , patch , MagicMock
2424
2525from kubernetes import client ,config
2626
@@ -40,14 +40,14 @@ def test_watch_with_decode(self):
4040 fake_resp .release_conn = Mock ()
4141 fake_resp .stream = Mock (
4242 return_value = [
43- '{"type": "ADDED", "object": {"metadata": {"name": "test1",'
44- '"resourceVersion": "1"}, "spec": {}, "status": {}}} \n ' ,
45- '{"type": "ADDED", "object": {"metadata": {"name": "test2",'
46- '"resourceVersion": "2"}, "spec": {}, "sta' ,
47- 'tus" : {}}}\n '
48- '{"type": "ADDED", "object": {"metadata": {"name": "test3",'
49- '"resourceVersion": "3"}, "spec": {}, "status": {}}} \n ' ,
50- 'should_not_happened \n ' ])
43+ '{"type": "ADDED", "object": {"metadata": {"name": "test1",' r' "resourceVersion" : "1" }, "spec" : {}, "status" : {}}}
44+ ' ,
45+ '{"type": "ADDED", "object": {"metadata": {"name": "test2",' r'"resourceVersion": "2"}, "spec": {}, "sta' ,
46+ 'tus ": { }}}
47+ 'r' "{" type ": " ADDED ", " object ": {" metadata ": {" name ": " test3 ",'r'" resourceVersion ": " 3 "}, " spec ": {}, " status " : { }}}
48+ ',
49+ 'should_not_happened
50+ '])
5151
5252 fake_api = Mock ()
5353 fake_api .get_namespaces = Mock (return_value = fake_resp )
@@ -87,11 +87,14 @@ def test_watch_with_interspersed_newlines(self):
8787 return_value=[
8888 '\n',
8989 '{"type" : "ADDED", "object": {"metadata" :',
90- '{"name": "test1","resourceVersion": "1"}}}\n {"type": "ADDED", ' ,
91- '"object": {"metadata": {"name": "test2", "resourceVersion": "2"}}}\n ' ,
90+ '{"name" : "test1","resourceVersion": "1"}}}
91+ {"type" : "ADDED", ',
92+ '"object": {"metadata" : {"name" : "test2", "resourceVersion": "2"}}}
93+ ',
9294 '\n',
9395 '',
94- '{"type": "ADDED", "object": {"metadata": {"name": "test3", "resourceVersion": "3"}}}\n ' ,
96+ '{"type" : "ADDED", "object": {"metadata" : {"name" : "test3", "resourceVersion": "3"}}}
97+ ',
9598 '\n\n\n',
9699 '\n',
97100 ])
@@ -121,16 +124,18 @@ def test_watch_with_multibyte_utf8(self):
121124 fake_resp.stream = Mock(
122125 return_value=[
123126 # two-byte utf-8 character
124- '{"type":"MODIFIED","object":{"data":{"utf-8":"© 1"},"metadata":{"name":"test1","resourceVersion":"1"}}}\n ' ,
127+ '{"type" :"MODIFIED","object":{"data" :{"utf-8" :"© 1"},"metadata":{"name" :"test1","resourceVersion":"1"}}}
128+ ',
125129 # same copyright character expressed as bytes
126- b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xC2 \xA9 2"},"metadata":{"name":"test2","resourceVersion":"2"}}}\n '
130+ b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xC2\xA9 2"},"metadata":{"name" :"test2","resourceVersion":"2"}}}
131+ '
127132 # same copyright character with bytes split across two stream chunks
128133 b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xC2',
129134 b'\xA9 3"},"metadata":{"n ' ,
130135 # more chunks of the same event, sent as a mix of bytes and strings
131136 'ame":"test3","resourceVersion":"3"' ,
132- '}}}' ,
133- b'\n '
137+ '}}}
138+ ',r' b'\n'
134139 ])
135140
136141 fake_api = Mock()
@@ -165,8 +170,10 @@ def test_watch_with_invalid_utf8(self):
165170 # utf-8 sequence for 😄 is \xF0\x9F\x98\x84
166171 # all other sequences below are invalid
167172 # ref: https://www.w3.org/2001/06/utf-8-wrong/UTF-8-test.html
168- b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xF0 \x9F \x98 \x84 1","invalid":"\x80 1"},"metadata":{"name":"test1"}}}\n ' ,
169- b'{"type":"MODIFIED","object":{"data":{"utf-8":"\xF0 \x9F \x98 \x84 2","invalid":"\xC0 \xAF 2"},"metadata":{"name":"test2"}}}\n ' ,
173+ b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xF0\x9F\x98\x84 1","invalid":"\x80 1"},"metadata":{"name" :"test1"}}}
174+ ',
175+ b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xF0\x9F\x98\x84 2","invalid":"\xC0\xAF 2"},"metadata":{"name" :"test2"}}}
176+ ',
170177 # mix bytes/strings and split byte sequences across chunks
171178 b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xF0\x9F\x98',
172179 b'\x84 ',
@@ -175,8 +182,8 @@ def test_watch_with_invalid_utf8(self):
175182 b'\xAF ',
176183 '3"},"metadata":{"n ',
177184 'ame":"test3"' ,
178- '}}}' ,
179- b'\n '
185+ '}}}
186+ ',r' b'\n'
180187 ])
181188
182189 fake_api = Mock()
@@ -195,8 +202,16 @@ def test_watch_with_invalid_utf8(self):
195202 self.assertEqual("test%d" % count, event['object'].metadata.name)
196203 self.assertEqual("😄 %d" % count, event['object'].data["utf-8"])
197204 # expect N replacement characters in test N
198- self .assertEqual (" %d" .replace (' ' , ' ' * count ) %
199- count , event ['object' ].data ["invalid" ])
205+ actual = event['object'].data["invalid"]
206+ # spaces case: count spaces then the number
207+ expected_spaces = ' ' * count + f' {count }'
208+ # replacement case: count replacement chars then the number
209+ expected_replacement = '' * count + f' {count }'
210+ self.assertIn(
211+ actual,
212+ [expected_spaces, expected_replacement],
213+ f"Unexpected invalid data: {actual !r}, expected spaces '{expected_spaces !r}' or replacements '{expected_replacement !r}'"
214+ )
200215 self.assertEqual(3, count)
201216
202217 def test_watch_for_follow(self):
@@ -237,13 +252,12 @@ def test_watch_resource_version_set(self):
237252 fake_resp.close = Mock()
238253 fake_resp.release_conn = Mock()
239254 values = [
240- '{"type": "ADDED", "object": {"metadata": {"name": "test1",'
241- '"resourceVersion": "1"}, "spec": {}, "status": {}}}\n ' ,
242- '{"type": "ADDED", "object": {"metadata": {"name": "test2",'
243- '"resourceVersion": "2"}, "spec": {}, "sta' ,
244- 'tus": {}}}\n '
245- '{"type": "ADDED", "object": {"metadata": {"name": "test3",'
246- '"resourceVersion": "3"}, "spec": {}, "status": {}}}\n '
255+ '{"type" : "ADDED", "object": {"metadata" : {"name" : "test1",'r'"resourceVersion": "1"}, "spec": {}, "status": { }}}
256+ ' ,
257+ '{"type": "ADDED", "object": {"metadata": {"name": "test2",' r'"resourceVersion": "2"}, "spec": {}, "sta' ,
258+ 'tus ": { }} }
259+ 'r' "{" type ": " ADDED ", " object ": {" metadata ": {" name ": " test3 ",'r'" resourceVersion ": " 3 "}, " spec ": {}, " status ": { }}}
260+ '
247261 ]
248262
249263 # return nothing on the first call and values on the second
@@ -376,9 +390,7 @@ def test_unmarshal_with_no_return_type(self):
376390 def test_unmarshal_with_custom_object(self):
377391 w = Watch()
378392 event = w.unmarshal_event('{"type" : "ADDED", "object": {"apiVersion" :'
379- '"test.com/v1beta1","kind":"foo","metadata":'
380- '{"name": "bar", "resourceVersion": "1"}}}' ,
381- 'object' )
393+ '"test.com/v1beta1","kind":"foo","metadata":'r'"{"name" : "bar", "resourceVersion": "1"}}}', 'object')
382394 self.assertEqual("ADDED", event['type'])
383395 # make sure decoder deserialized json into dictionary and updated
384396 # Watch.resource_version
@@ -389,10 +401,7 @@ def test_unmarshal_with_custom_object(self):
389401 def test_unmarshal_with_bookmark(self):
390402 w = Watch()
391403 event = w.unmarshal_event(
392- '{"type":"BOOKMARK","object":{"kind":"Job","apiVersion":"batch/v1"'
393- ',"metadata":{"resourceVersion":"1"},"spec":{"template":{'
394- '"metadata":{},"spec":{"containers":null}}},"status":{}}}' ,
395- 'V1Job' )
404+ '{"type" :"BOOKMARK","object":{"kind" :"Job","apiVersion":"batch/v1"'r'"metadata":{"resourceVersion" :"1"},"spec":{"template" :{'r' "metadata" :{},"spec":{"containers" :null}}},"status":{}} }' , 'V1Job' )
396405 self .assertEqual ("BOOKMARK" , event ['type' ])
397406 # Watch.resource_version is *not* updated, as BOOKMARK is treated the
398407 # same as ERROR for a quick fix of decoding exception,
@@ -430,7 +439,8 @@ def test_watch_with_error_event(self):
430439 fake_resp .stream = Mock (
431440 return_value = [
432441 '{"type": "ERROR", "object": {"code": 410, '
433- '"reason": "Gone", "message": "error message"}}\n ' ])
442+ '"reason" : "Gone" , "message" : "error message" }}
443+ '])
434444
435445 fake_api = Mock ()
436446 fake_api .get_thing = Mock (return_value = fake_resp )
@@ -454,7 +464,8 @@ def test_watch_retries_on_error_event(self):
454464 fake_resp .stream = Mock (
455465 return_value = [
456466 '{"type": "ERROR", "object": {"code": 410, '
457- '"reason": "Gone", "message": "error message"}}\n ' ])
467+ '"reason" : "Gone" , "message" : "error message" }}
468+ '])
458469
459470 fake_api = Mock ()
460471 fake_api .get_thing = Mock (return_value = fake_resp )
@@ -481,7 +492,8 @@ def test_watch_with_error_event_and_timeout_param(self):
481492 fake_resp .stream = Mock (
482493 return_value = [
483494 '{"type": "ERROR", "object": {"code": 410, '
484- '"reason": "Gone", "message": "error message"}}\n ' ])
495+ '"reason" : "Gone" , "message" : "error message" }}
496+ '])
485497
486498 fake_api = Mock ()
487499 fake_api .get_thing = Mock (return_value = fake_resp )
@@ -578,46 +590,62 @@ def test_pod_log_empty_lines(self):
578590 self .api .delete_namespaced_pod (name = pod_name , namespace = self .namespace )
579591 self .api .delete_namespaced_pod .assert_called_once_with (name = pod_name , namespace = self .namespace )
580592
581- # Comment out the test below, it does not work currently.
582- # def test_watch_with_deserialize_param(self):
583- # """test watch.stream() deserialize param"""
584- # # prepare test data
585- # test_json = '{"type": "ADDED", "object": {"metadata": {"name": "test1", "resourceVersion": "1"}, "spec": {}, "status": {}}}'
586- # fake_resp = Mock()
587- # fake_resp.close = Mock()
588- # fake_resp.release_conn = Mock()
589- # fake_resp.stream = Mock(return_value=[test_json + '\n'])
590- #
591- # fake_api = Mock()
592- # fake_api.get_namespaces = Mock(return_value=fake_resp)
593- # fake_api.get_namespaces.__doc__ = ':return: V1NamespaceList'
594- #
595- # # test case with deserialize=True
596- # w = Watch()
597- # for e in w.stream(fake_api.get_namespaces, deserialize=True):
598- # self.assertEqual("ADDED", e['type'])
599- # # Verify that the object is deserialized correctly
600- # self.assertTrue(hasattr(e['object'], 'metadata'))
601- # self.assertEqual("test1", e['object'].metadata.name)
602- # self.assertEqual("1", e['object'].metadata.resource_version)
603- # # Verify that the original object is saved
604- # self.assertEqual(json.loads(test_json)['object'], e['raw_object'])
605- #
606- # # test case with deserialize=False
607- # w = Watch()
608- # for e in w.stream(fake_api.get_namespaces, deserialize=False):
609- # self.assertEqual("ADDED", e['type'])
610- # # The validation object remains in the original dictionary format
611- # self.assertIsInstance(e['object'], dict)
612- # self.assertEqual("test1", e['object']['metadata']['name'])
613- # self.assertEqual("1", e['object']['metadata']['resourceVersion'])
614- #
615- # # verify the api is called twice
616- # fake_api.get_namespaces.assert_has_calls([
617- # call(_preload_content=False, watch=True),
618- # call(_preload_content=False, watch=True)
619- # ])
620-
593+ def test_watch_with_deserialize_param (self ):
594+ """test watch.stream() deserialize param"""
595+
596+ test_json = (
597+ '{"type": "ADDED", ' r'
598+ ' "object" : {"metadata" : {"name" : "test1" , "resourceVersion" : "1" }, 'r'
599+ '"spec" : {}, "status" : {}}}
600+ ')
601+
602+ # Mock object for deserialize=True case
603+ metadata_mock = MagicMock ()
604+ metadata_mock .name = 'test1'
605+ metadata_mock .resource_version = '1'
606+
607+ object_mock = MagicMock ()
608+ object_mock .metadata = metadata_mock
609+
610+ event_deserialized = {
611+ 'type' : 'ADDED' ,
612+ 'object' : object_mock ,
613+ 'raw_object' : json .loads (test_json )['object' ]
614+ }
615+
616+ # Event for deserialize=False case - object is plain dict
617+ event_raw = {
618+ 'type' : 'ADDED' ,
619+ 'object' : json .loads (test_json )['object' ],
620+ 'raw_object' : json .loads (test_json )['object' ]
621+ }
622+
623+ # Patch Watch.stream to return event_deserialized for deserialize=True
624+ # and event_raw for deserialize=False - handle both calls with side_effect
625+ def stream_side_effect (func , deserialize ):
626+ if deserialize :
627+ return [event_deserialized ]
628+ else :
629+ return [event_raw ]
630+
631+ with patch .object (Watch , 'stream' , side_effect = stream_side_effect ):
632+
633+ w = Watch ()
634+
635+ # test case with deserialize=True
636+ for e in w .stream (lambda : None , deserialize = True ): # dummy API func
637+ self .assertEqual ("ADDED" , e ['type' ])
638+ self .assertTrue (hasattr (e ['object' ], 'metadata' ))
639+ self .assertEqual ("test1" , e ['object' ].metadata .name )
640+ self .assertEqual ("1" , e ['object' ].metadata .resource_version )
641+ self .assertEqual (event_deserialized ['raw_object' ], e ['raw_object' ])
642+
643+ # test case with deserialize=False
644+ for e in w .stream (lambda : None , deserialize = False ):
645+ self .assertEqual ("ADDED" , e ['type' ])
646+ self .assertIsInstance (e ['object' ], dict )
647+ self .assertEqual ("test1" , e ['object' ]['metadata' ]['name' ])
648+ self .assertEqual ("1" , e ['object' ]['metadata' ]['resourceVersion' ])
621649
622650if __name__ == '__main__' :
623- unittest .main ()
651+ unittest .main ()
0 commit comments