20
20
21
21
import json
22
22
23
- from unittest .mock import Mock , call
23
+ from unittest .mock import Mock , call , patch , MagicMock
24
24
25
25
from kubernetes import client ,config
26
26
@@ -40,14 +40,14 @@ def test_watch_with_decode(self):
40
40
fake_resp .release_conn = Mock ()
41
41
fake_resp .stream = Mock (
42
42
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
+ '])
51
51
52
52
fake_api = Mock ()
53
53
fake_api .get_namespaces = Mock (return_value = fake_resp )
@@ -87,11 +87,14 @@ def test_watch_with_interspersed_newlines(self):
87
87
return_value=[
88
88
'\n',
89
89
'{"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
+ ',
92
94
'\n',
93
95
'',
94
- '{"type": "ADDED", "object": {"metadata": {"name": "test3", "resourceVersion": "3"}}}\n ' ,
96
+ '{"type" : "ADDED", "object": {"metadata" : {"name" : "test3", "resourceVersion": "3"}}}
97
+ ',
95
98
'\n\n\n',
96
99
'\n',
97
100
])
@@ -121,16 +124,18 @@ def test_watch_with_multibyte_utf8(self):
121
124
fake_resp.stream = Mock(
122
125
return_value=[
123
126
# 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
+ ',
125
129
# 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
+ '
127
132
# same copyright character with bytes split across two stream chunks
128
133
b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xC2',
129
134
b'\xA9 3"},"metadata":{"n ' ,
130
135
# more chunks of the same event, sent as a mix of bytes and strings
131
136
'ame":"test3","resourceVersion":"3"' ,
132
- '}}}' ,
133
- b'\n '
137
+ '}}}
138
+ ',r' b'\n'
134
139
])
135
140
136
141
fake_api = Mock()
@@ -165,8 +170,10 @@ def test_watch_with_invalid_utf8(self):
165
170
# utf-8 sequence for 😄 is \xF0\x9F\x98\x84
166
171
# all other sequences below are invalid
167
172
# 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
+ ',
170
177
# mix bytes/strings and split byte sequences across chunks
171
178
b'{"type" :"MODIFIED","object":{"data" :{"utf-8" :"\xF0\x9F\x98',
172
179
b'\x84 ',
@@ -175,8 +182,8 @@ def test_watch_with_invalid_utf8(self):
175
182
b'\xAF ',
176
183
'3"},"metadata":{"n ',
177
184
'ame":"test3"' ,
178
- '}}}' ,
179
- b'\n '
185
+ '}}}
186
+ ',r' b'\n'
180
187
])
181
188
182
189
fake_api = Mock()
@@ -195,8 +202,16 @@ def test_watch_with_invalid_utf8(self):
195
202
self.assertEqual("test%d" % count, event['object'].metadata.name)
196
203
self.assertEqual("😄 %d" % count, event['object'].data["utf-8"])
197
204
# 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
+ )
200
215
self.assertEqual(3, count)
201
216
202
217
def test_watch_for_follow(self):
@@ -237,13 +252,12 @@ def test_watch_resource_version_set(self):
237
252
fake_resp.close = Mock()
238
253
fake_resp.release_conn = Mock()
239
254
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
+ '
247
261
]
248
262
249
263
# return nothing on the first call and values on the second
@@ -376,9 +390,7 @@ def test_unmarshal_with_no_return_type(self):
376
390
def test_unmarshal_with_custom_object(self):
377
391
w = Watch()
378
392
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')
382
394
self.assertEqual("ADDED", event['type'])
383
395
# make sure decoder deserialized json into dictionary and updated
384
396
# Watch.resource_version
@@ -389,10 +401,7 @@ def test_unmarshal_with_custom_object(self):
389
401
def test_unmarshal_with_bookmark(self):
390
402
w = Watch()
391
403
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' )
396
405
self .assertEqual ("BOOKMARK" , event ['type' ])
397
406
# Watch.resource_version is *not* updated, as BOOKMARK is treated the
398
407
# same as ERROR for a quick fix of decoding exception,
@@ -430,7 +439,8 @@ def test_watch_with_error_event(self):
430
439
fake_resp .stream = Mock (
431
440
return_value = [
432
441
'{"type": "ERROR", "object": {"code": 410, '
433
- '"reason": "Gone", "message": "error message"}}\n ' ])
442
+ '"reason" : "Gone" , "message" : "error message" }}
443
+ '])
434
444
435
445
fake_api = Mock ()
436
446
fake_api .get_thing = Mock (return_value = fake_resp )
@@ -454,7 +464,8 @@ def test_watch_retries_on_error_event(self):
454
464
fake_resp .stream = Mock (
455
465
return_value = [
456
466
'{"type": "ERROR", "object": {"code": 410, '
457
- '"reason": "Gone", "message": "error message"}}\n ' ])
467
+ '"reason" : "Gone" , "message" : "error message" }}
468
+ '])
458
469
459
470
fake_api = Mock ()
460
471
fake_api .get_thing = Mock (return_value = fake_resp )
@@ -481,7 +492,8 @@ def test_watch_with_error_event_and_timeout_param(self):
481
492
fake_resp .stream = Mock (
482
493
return_value = [
483
494
'{"type": "ERROR", "object": {"code": 410, '
484
- '"reason": "Gone", "message": "error message"}}\n ' ])
495
+ '"reason" : "Gone" , "message" : "error message" }}
496
+ '])
485
497
486
498
fake_api = Mock ()
487
499
fake_api .get_thing = Mock (return_value = fake_resp )
@@ -578,46 +590,62 @@ def test_pod_log_empty_lines(self):
578
590
self .api .delete_namespaced_pod (name = pod_name , namespace = self .namespace )
579
591
self .api .delete_namespaced_pod .assert_called_once_with (name = pod_name , namespace = self .namespace )
580
592
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' ])
621
649
622
650
if __name__ == '__main__' :
623
- unittest .main ()
651
+ unittest .main ()
0 commit comments