12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- from asynctest import CoroutineMock , Mock , TestCase , patch
15
+ import json
16
+
17
+ from asynctest import CoroutineMock , Mock , TestCase
16
18
17
19
import kubernetes_asyncio
18
20
from kubernetes_asyncio .watch import Watch
@@ -23,30 +25,68 @@ class WatchTest(TestCase):
23
25
async def test_watch_with_decode (self ):
24
26
fake_resp = CoroutineMock ()
25
27
fake_resp .content .readline = CoroutineMock ()
26
- fake_resp .content .readline .side_effect = [
27
- '{"type": "ADDED", "object": {"metadata": {"name": "test1"},"spec": {}, "status": {}}}' ,
28
- '{"type": "ADDED", "object": {"metadata": {"name": "test2"},"spec": {}, "status": {}}}' ,
29
- '{"type": "ADDED", "object": {"metadata": {"name": "test3"},"spec": {}, "status": {}}}' ,
30
- 'should_not_happened' ]
28
+ side_effects = [
29
+ {
30
+ "type" : "ADDED" ,
31
+ "object" : {
32
+ "metadata" : {"name" : "test{}" .format (uid )},
33
+ "spec" : {}, "status" : {}
34
+ }
35
+ }
36
+ for uid in range (3 )
37
+ ]
38
+ side_effects = [json .dumps (_ ).encode ('utf8' ) for _ in side_effects ]
39
+ side_effects .extend ([AssertionError ('Should not have been called' )])
40
+ fake_resp .content .readline .side_effect = side_effects
31
41
32
42
fake_api = Mock ()
33
43
fake_api .get_namespaces = CoroutineMock (return_value = fake_resp )
34
44
fake_api .get_namespaces .__doc__ = ':return: V1NamespaceList'
35
45
36
46
watch = kubernetes_asyncio .watch .Watch ()
37
- count = 1
38
- async for e in watch .stream (fake_api .get_namespaces ):
47
+ count = 0
48
+ async for e in watch .stream (fake_api .get_namespaces , resource_version = '123' ):
39
49
self .assertEqual ("ADDED" , e ['type' ])
40
50
# make sure decoder worked and we got a model with the right name
41
51
self .assertEqual ("test%d" % count , e ['object' ].metadata .name )
52
+
53
+ # Stop the watch. This must not return the next event which would
54
+ # be an AssertionError exception.
42
55
count += 1
43
- # make sure we can stop the watch and the last event with won't be
44
- # returned
45
- if count == 4 :
56
+ if count == len (side_effects ) - 1 :
46
57
watch .stop ()
47
58
48
59
fake_api .get_namespaces .assert_called_once_with (
49
- _preload_content = False , watch = True )
60
+ _preload_content = False , watch = True , resource_version = '123' )
61
+
62
+ async def test_watch_k8s_empty_response (self ):
63
+ """Stop the iterator when the response is empty.
64
+
65
+ This typically happens when the user supplied timeout expires.
66
+
67
+ """
68
+ # Mock the readline return value to first return a valid response
69
+ # followed by an empty response.
70
+ fake_resp = CoroutineMock ()
71
+ fake_resp .content .readline = CoroutineMock ()
72
+ side_effects = [
73
+ {"type" : "ADDED" , "object" : {"metadata" : {"name" : "test0" }, "spec" : {}, "status" : {}}},
74
+ {"type" : "ADDED" , "object" : {"metadata" : {"name" : "test1" }, "spec" : {}, "status" : {}}},
75
+ ]
76
+ side_effects = [json .dumps (_ ).encode ('utf8' ) for _ in side_effects ]
77
+ fake_resp .content .readline .side_effect = side_effects + [b'' ]
78
+
79
+ # Fake the K8s resource object to watch.
80
+ fake_api = Mock ()
81
+ fake_api .get_namespaces = CoroutineMock (return_value = fake_resp )
82
+ fake_api .get_namespaces .__doc__ = ':return: V1NamespaceList'
83
+
84
+ # Iteration must cease after all valid responses were received.
85
+ watch = kubernetes_asyncio .watch .Watch ()
86
+ cnt = 0
87
+ async for _ in watch .stream (fake_api .get_namespaces ):
88
+ cnt += 1
89
+ self .assertEqual (cnt , len (side_effects ))
50
90
51
91
def test_unmarshal_with_float_object (self ):
52
92
w = Watch ()
@@ -64,6 +104,29 @@ def test_unmarshal_with_no_return_type(self):
64
104
self .assertEqual (["test1" ], event ['object' ])
65
105
self .assertEqual (["test1" ], event ['raw_object' ])
66
106
107
+ async def test_unmarshall_k8s_error_response (self ):
108
+ """Never parse messages of type ERROR.
109
+
110
+ This test uses an actually recorded error, in this case for an outdated
111
+ resource version.
112
+
113
+ """
114
+ # An actual error response sent by K8s during testing.
115
+ k8s_err = {
116
+ 'type' : 'ERROR' ,
117
+ 'object' : {
118
+ 'kind' : 'Status' , 'apiVersion' : 'v1' , 'metadata' : {},
119
+ 'status' : 'Failure' ,
120
+ 'message' : 'too old resource version: 1 (8146471)' ,
121
+ 'reason' : 'Gone' , 'code' : 410
122
+ }
123
+ }
124
+
125
+ ret = Watch ().unmarshal_event (json .dumps (k8s_err ), None )
126
+ self .assertEqual (ret ['type' ], k8s_err ['type' ])
127
+ self .assertEqual (ret ['object' ], k8s_err ['object' ])
128
+ self .assertEqual (ret ['object' ], k8s_err ['object' ])
129
+
67
130
async def test_watch_with_exception (self ):
68
131
fake_resp = CoroutineMock ()
69
132
fake_resp .content .readline = CoroutineMock ()
0 commit comments