Skip to content

Commit 8377e67

Browse files
Fix: Windows compatibility for development and test suite (stream, watch, symlink, temp file handling)
1 parent 0c189ce commit 8377e67

31 files changed

+6508
-84
lines changed

kubernetes/base/watch/watch_test.py

Lines changed: 108 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import json
2222

23-
from unittest.mock import Mock, call
23+
from unittest.mock import Mock, call, patch, MagicMock
2424

2525
from 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

622650
if __name__ == '__main__':
623-
unittest.main()
651+
unittest.main()

0 commit comments

Comments
 (0)