1- from typing import Iterable
1+ import time
2+ from typing import Callable , Iterable , Protocol , Tuple
23import unittest
34from unittest import mock
45from absl .testing import flagsaver
56from perfkitbenchmarker import container_service
67from perfkitbenchmarker import errors
78from perfkitbenchmarker import provider_info
9+ from perfkitbenchmarker import vm_util
810from perfkitbenchmarker .configs import container_spec
911from perfkitbenchmarker .sample import Sample
1012from tests import pkb_common_test_case
@@ -25,6 +27,60 @@ def _Delete(self):
2527 pass
2628
2729
30+ kubectl_timeout_tuple = (
31+ '' ,
32+ (
33+ 'Unable to connect to the server: dial tcp 10.42.42.42:443:'
34+ 'connect: connection timed out'
35+ ),
36+ 1 ,
37+ )
38+
39+
40+ class _IssueCommandCallable (Protocol ):
41+
42+ def __call__ (
43+ self ,
44+ cmd : Iterable [str ],
45+ suppress_failure : Callable [[str , str , int ], bool ] | None = None ,
46+ ** kwargs ,
47+ ) -> Tuple [str , str , int ]:
48+ ...
49+
50+
51+ def _MockedIssueCommandSuppressing (
52+ stderr : str ,
53+ ) -> _IssueCommandCallable :
54+ def _MockedCommand (
55+ cmd : Iterable [str ],
56+ suppress_failure : Callable [[str , str , int ], bool ] | None = None ,
57+ ** kwargs ,
58+ ):
59+ _ = cmd
60+ _ = kwargs
61+ stdout = ''
62+ status = 1
63+ if suppress_failure and suppress_failure (stdout , stderr , status ):
64+ return stdout , '' , 0
65+ return stdout , stderr , status
66+
67+ return _MockedCommand
68+
69+
70+ def _MockedIssueCommandFailure (
71+ cmd : Iterable [str ],
72+ suppress_failure : Callable [[str , str , int ], bool ] | None = None ,
73+ ** kwargs ,
74+ ) -> Tuple [str , str , int ]:
75+ return _MockedIssueCommandSuppressing (
76+ stderr = 'A failure occurred' ,
77+ )(
78+ cmd ,
79+ suppress_failure = suppress_failure ,
80+ ** kwargs ,
81+ )
82+
83+
2884class ContainerServiceTest (pkb_common_test_case .PkbCommonTestCase ):
2985
3086 def setUp (self ):
@@ -62,27 +118,60 @@ def test_apply_manifest_gets_deployment_name(self):
62118 )
63119 self .assertEqual (next (deploy_ids ), 'deployment.apps/test-deployment' )
64120
65- def test_retriable_kubectl_command_fails_on_random_error (self ):
66- self .MockIssueCommand (
67- {'get podpatchwork' : [('' , 'error: invalid syntax' , 1 )]}
68- )
121+ @mock .patch .object (
122+ vm_util ,
123+ 'IssueCommand' ,
124+ side_effect = [errors .VmUtil .IssueCommandError ()],
125+ autospec = True ,
126+ )
127+ def test_retriable_kubectl_command_fails_on_random_error (self , _ ):
69128 with self .assertRaises (errors .VmUtil .IssueCommandError ):
70129 container_service .RunRetryableKubectlCommand (['get' , 'podpatchwork' ])
71130
72- def test_retriable_kubectl_command_retries_on_connection_reset (self ):
73- self .MockIssueCommand ({
74- 'get pods' : [
75- ('' , 'error: read: connection reset by peer' , 1 ),
76- ('pod1, pod2' , '' , 0 ),
77- ]
78- })
131+ @mock .patch .object (
132+ vm_util ,
133+ 'IssueCommand' ,
134+ side_effect = [
135+ errors .VmUtil .IssueCommandTimeoutError (),
136+ ('pod1, pod2' , '' , 0 ),
137+ ],
138+ autospec = True ,
139+ )
140+ @mock .patch .object (time , 'sleep' , autospec = True )
141+ def test_retriable_kubectl_command_retries_on_retriable_error (
142+ self , sleep_mock , issue_command_mock
143+ ):
79144 out , err , ret = container_service .RunRetryableKubectlCommand (
80145 ['get' , 'pods' ]
81146 )
82147 self .assertEqual (out , 'pod1, pod2' )
83148 self .assertEqual (err , '' )
84149 self .assertEqual (ret , 0 )
85150
151+ def test_retriable_kubectl_command_passes_timeout_through (self ):
152+ def _VerifyTimeout (
153+ cmd : Iterable [str ],
154+ timeout : int | None = vm_util .DEFAULT_TIMEOUT ,
155+ ** kwargs ,
156+ ) -> Tuple [str , str , int ]:
157+ _ = cmd
158+ _ = kwargs
159+ self .assertEqual (
160+ timeout ,
161+ 1 ,
162+ 'timeout not correctly passed to underlying vm_util.IssueCommand()' ,
163+ )
164+ return 'ok' , '' , 0
165+
166+ with mock .patch .object (vm_util , 'IssueCommand' , _VerifyTimeout ):
167+ container_service .RunRetryableKubectlCommand (['get' , 'pods' ], timeout = 1 )
168+
169+ def test_retriable_kubectl_command_fails_with_raise_on_timeout (self ):
170+ with self .assertRaises (ValueError ):
171+ container_service .RunRetryableKubectlCommand (
172+ ['get' , 'pods' ], raise_on_timeout = True
173+ )
174+
86175 def test_GetNumReplicasSamples_found (self ):
87176 resource_name = 'deployment/my_deployment'
88177 namespace = 'my_namespace'
@@ -161,6 +250,63 @@ def _Sample(count: int, state: str) -> Sample:
161250 ],
162251 )
163252
253+ @mock .patch .object (
254+ vm_util ,
255+ 'IssueCommand' ,
256+ return_value = ['stdout' , 'stderr' , 0 ],
257+ autospec = True ,
258+ )
259+ def test_RunKubectlCommand (self , issue_command_mock ):
260+ stdout , stderr , status = container_service .RunKubectlCommand (
261+ ['get' , 'pods' ]
262+ )
263+ self .assertEqual (stdout , 'stdout' )
264+ self .assertEqual (stderr , 'stderr' )
265+ self .assertEqual (status , 0 )
266+
267+ @mock .patch .object (
268+ vm_util ,
269+ 'IssueCommand' ,
270+ side_effect = errors .VmUtil .IssueCommandTimeoutError (),
271+ autospec = True ,
272+ )
273+ def test_RunKubectlCommand_CommandTimeoutPropagated (self , issue_command_mock ):
274+ with self .assertRaises (errors .VmUtil .IssueCommandTimeoutError ):
275+ container_service .RunKubectlCommand (['get' , 'pods' ])
276+
277+ def test_RunKubectlCommand_KubectlTimeoutRaisesCommandTimeout (self ):
278+ for err in container_service ._RETRYABLE_KUBECTL_ERRORS :
279+ with mock .patch .object (
280+ vm_util , 'IssueCommand' , _MockedIssueCommandSuppressing (stderr = err )
281+ ):
282+ with self .assertRaises (
283+ errors .VmUtil .IssueCommandTimeoutError ,
284+ msg = f'Failed to raise timeout for error: { err } ' ,
285+ ):
286+ container_service .RunKubectlCommand (['get' , 'pods' ])
287+
288+ def test_RunKubectlCommand_KubectlTimeoutWithSuppressFailureRaisesCommandTimeout (
289+ self ,
290+ ):
291+ for err in container_service ._RETRYABLE_KUBECTL_ERRORS :
292+ with mock .patch .object (
293+ vm_util , 'IssueCommand' , _MockedIssueCommandSuppressing (stderr = err )
294+ ):
295+ with self .assertRaises (
296+ errors .VmUtil .IssueCommandTimeoutError ,
297+ msg = f'Failed to raise timeout for error: { err } ' ,
298+ ):
299+ container_service .RunKubectlCommand (
300+ ['get' , 'pods' ], suppress_failure = lambda x , y , z : True
301+ )
302+
303+ @mock .patch .object (vm_util , 'IssueCommand' , _MockedIssueCommandFailure )
304+ def test_RunKubectlCommand_KubectlFailWithSuppressFailure (self ):
305+ _ , _ , status = container_service .RunKubectlCommand (
306+ ['get' , 'pods' ], suppress_failure = lambda x , y , z : True
307+ )
308+ self .assertEqual (status , 0 )
309+
164310
165311def _ClearTimestamps (samples : Iterable [Sample ]) -> Iterable [Sample ]:
166312 for s in samples :
0 commit comments