11"""Tests related to pycloudlib.instance module."""
22
33from itertools import repeat
4- from unittest import mock
54
65import pytest
76from paramiko import SSHException
1514
1615
1716@pytest .fixture
18- def concrete_instance_cls ():
17+ def concrete_instance_cls (mocker ):
1918 """Return a BaseInstance subclass which can be instantiated.
2019
2120 Source: https://stackoverflow.com/a/28738073
2221 """
23- with mock .patch .object (BaseInstance , "__abstractmethods__" , set ()):
24- yield BaseInstance
22+ mocker .patch .object (BaseInstance , "__abstractmethods__" , set ())
23+ return BaseInstance
2524
2625
2726class TestWait :
2827 """Tests covering pycloudlib.instance.Instance.wait."""
2928
30- def test_wait (self , concrete_instance_cls ):
29+ def test_wait (self , concrete_instance_cls , mocker ):
3130 """Test wait calls the two methods it should with correct passthrough.
3231
3332 (`None` is used to test the default.)
3433 """
3534 instance = concrete_instance_cls (key_pair = None )
36- with mock .patch .multiple (
35+ mocks = mocker .patch .multiple (
3736 instance ,
38- _wait_for_instance_start = mock .DEFAULT ,
39- _wait_for_execute = mock .DEFAULT ,
40- _wait_for_cloudinit = mock .DEFAULT ,
41- ) as mocks :
42- instance .wait ()
37+ _wait_for_instance_start = mocker .DEFAULT ,
38+ _wait_for_execute = mocker .DEFAULT ,
39+ _wait_for_cloudinit = mocker .DEFAULT ,
40+ )
41+ instance .wait ()
4342
4443 assert 1 == mocks ["_wait_for_instance_start" ].call_count
4544 assert 1 == mocks ["_wait_for_execute" ].call_count
@@ -52,25 +51,24 @@ def test_wait(self, concrete_instance_cls):
5251 pytest .param (SSHException , id = "exception" ),
5352 ],
5453 )
55- @mock .patch .object (BaseInstance , "execute" )
56- @mock .patch ("pycloudlib.instance.time.sleep" )
57- @mock .patch ("pycloudlib.instance.time.time" )
58- @mock .patch ("logging.Logger.debug" )
5954 def test_wait_execute_failure (
6055 self ,
61- m_debug ,
62- m_time ,
63- m_sleep ,
64- m_execute ,
6556 execute_effect ,
6657 concrete_instance_cls ,
58+ mocker ,
6759 ):
6860 """Test wait calls when execute command fails."""
61+ mocker .patch ("logging.Logger.debug" )
62+ mocker .patch ("logging.Logger.info" )
63+ m_time = mocker .patch ("pycloudlib.instance.time.time" )
64+ m_sleep = mocker .patch ("pycloudlib.instance.time.sleep" )
65+ m_execute = mocker .patch .object (BaseInstance , "execute" )
66+
6967 instance = concrete_instance_cls (key_pair = None )
70- m_time .side_effect = [1 , 1 , 2 , 10 * 60 , 10 * 60 + 1 ]
68+ m_time .side_effect = [1 , 1 , 2 , 10 * 60 + 1 ]
7169 m_execute .side_effect = execute_effect
7270 expected_msg = "Instance can't be reached after 10 minutes. Failed to obtain new boot id"
73- expected_call_args = [mock .call ("cat /proc/sys/kernel/random/boot_id" , no_log = True )] * 2
71+ expected_call_args = [mocker .call ("cat /proc/sys/kernel/random/boot_id" , no_log = True )] * 2
7472
7573 with pytest .raises (PycloudlibTimeoutError ) as excinfo :
7674 instance .wait ()
@@ -80,89 +78,77 @@ def test_wait_execute_failure(
8078 assert expected_call_args == m_execute .call_args_list
8179
8280
83- @mock .patch ("pycloudlib.instance.BaseInstance._do_restart" )
84- @mock .patch ("pycloudlib.instance.BaseInstance.get_boot_id" )
85- @mock .patch ("pycloudlib.instance.BaseInstance.wait" )
86- @mock .patch ("pycloudlib.instance.BaseInstance.wait_for_restart" )
8781class TestRestart :
8882 """Test base restart behavior."""
8983
9084 @pytest .fixture (autouse = True )
91- def unchecked_mocks (self ):
85+ def setup_mocks (self , mocker ):
9286 """Mock things we don't want as test parameters."""
93- with mock .patch ("pycloudlib.instance.BaseInstance._sync_filesystem" ):
94- yield
87+ mocker .patch ("pycloudlib.instance.BaseInstance._sync_filesystem" )
88+ self .m_wait_for_restart = mocker .patch ("pycloudlib.instance.BaseInstance.wait_for_restart" )
89+ self .m_wait = mocker .patch ("pycloudlib.instance.BaseInstance.wait" )
90+ self .m_boot_id = mocker .patch ("pycloudlib.instance.BaseInstance.get_boot_id" )
91+ self .m_do_restart = mocker .patch ("pycloudlib.instance.BaseInstance._do_restart" )
9592
9693 def test_no_wait (
9794 self ,
98- m_wait_for_restart ,
99- m_wait ,
100- m_boot_id ,
101- m_do_restart ,
10295 concrete_instance_cls ,
10396 ):
10497 """Test wait=False."""
10598 instance = concrete_instance_cls (key_pair = None )
10699 instance .restart (wait = False )
107- assert m_do_restart .call_count == 1
108- assert m_boot_id .call_count == 0
109- assert m_wait_for_restart .call_count == 0
110- assert m_wait .call_count == 0
100+ assert self . m_do_restart .call_count == 1
101+ assert self . m_boot_id .call_count == 0
102+ assert self . m_wait_for_restart .call_count == 0
103+ assert self . m_wait .call_count == 0
111104
112105 def test_instance_not_reachable (
113106 self ,
114- m_wait_for_restart ,
115- m_wait ,
116- m_boot_id ,
117- m_do_restart ,
118107 concrete_instance_cls ,
119108 ):
120109 """Test when instance is not reachable."""
121110 instance = concrete_instance_cls (key_pair = None )
122- m_boot_id .side_effect = SSHException
111+ self . m_boot_id .side_effect = SSHException
123112 instance .restart (wait = True )
124- assert m_do_restart .call_count == 1
125- assert m_wait_for_restart .call_count == 0
126- assert m_wait .call_count == 1
113+ assert self . m_do_restart .call_count == 1
114+ assert self . m_wait_for_restart .call_count == 0
115+ assert self . m_wait .call_count == 1
127116
128117 def test_instance_reachable (
129118 self ,
130- m_wait_for_restart ,
131- m_wait ,
132- m_boot_id ,
133- m_do_restart ,
134119 concrete_instance_cls ,
135120 ):
136121 """Test when instance is reachable."""
137122 instance = concrete_instance_cls (key_pair = None )
138- m_boot_id .side_effect = Result ("11111111-1111-1111-1111-111111111111" , "" , 0 )
123+ self . m_boot_id .side_effect = Result ("11111111-1111-1111-1111-111111111111" , "" , 0 )
139124 instance .restart (wait = True )
140- assert m_do_restart .call_count == 1
141- assert m_wait_for_restart .call_count == 1
142- assert m_wait .call_count == 0
125+ assert self . m_do_restart .call_count == 1
126+ assert self . m_wait_for_restart .call_count == 1
127+ assert self . m_wait .call_count == 0
143128
144129
145130class TestWaitForRestart :
146131 """Tests covering pycloudlib.instance.Instance.wait_for_restart."""
147132
148- @mock .patch .object (
149- BaseInstance ,
150- "execute" ,
151- side_effect = [
152- Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
153- Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
154- Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
155- Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
156- Result ("22222222-2222-2222-2222-222222222222" , "" , 0 ),
157- ],
158- )
159- @mock .patch .object (BaseInstance , "_wait_for_cloudinit" )
160- @mock .patch ("pycloudlib.instance.time.sleep" )
161- @mock .patch ("pycloudlib.instance.time.time" , return_value = 1 )
162133 def test_wait_for_restart (
163- self , _m_time , _m_sleep , _m_wait_ci , m_execute , concrete_instance_cls
134+ self , concrete_instance_cls , mocker
164135 ):
165136 """Test wait calls _wait_for_execute and waits till differing."""
137+ mocker .patch ("pycloudlib.instance.time.time" , return_value = 1 )
138+ mocker .patch ("pycloudlib.instance.time.sleep" )
139+ mocker .patch .object (BaseInstance , "_wait_for_cloudinit" )
140+ m_execute = mocker .patch .object (
141+ BaseInstance ,
142+ "execute" ,
143+ side_effect = [
144+ Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
145+ Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
146+ Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
147+ Result ("11111111-1111-1111-1111-111111111111" , "" , 0 ),
148+ Result ("22222222-2222-2222-2222-222222222222" , "" , 0 ),
149+ ],
150+ )
151+
166152 instance = concrete_instance_cls (key_pair = None )
167153 instance .wait_for_restart (old_boot_id = "11111111-1111-1111-1111-111111111111" )
168154 assert m_execute .call_count == 5
@@ -174,25 +160,24 @@ def test_wait_for_restart(
174160 repeat (Result ("11111111-1111-1111-1111-111111111111" , "" , 0 )),
175161 ],
176162 )
177- @mock .patch .object (BaseInstance , "execute" )
178- @mock .patch ("pycloudlib.instance.time.sleep" )
179- @mock .patch ("pycloudlib.instance.time.time" )
180- @mock .patch ("logging.Logger.debug" )
181163 def test_boot_id_failure (
182164 self ,
183- m_debug ,
184- m_time ,
185- m_sleep ,
186- m_execute ,
187165 execute_side_effect ,
188166 concrete_instance_cls ,
167+ mocker ,
189168 ):
190169 """Test wait calls when execute command fails."""
170+ mocker .patch ("logging.Logger.debug" )
171+ mocker .patch ("logging.Logger.info" )
172+ m_time = mocker .patch ("pycloudlib.instance.time.time" )
173+ m_sleep = mocker .patch ("pycloudlib.instance.time.sleep" )
174+ m_execute = mocker .patch .object (BaseInstance , "execute" )
175+
191176 m_execute .side_effect = execute_side_effect
192177 instance = concrete_instance_cls (key_pair = None )
193- m_time .side_effect = [1 , 1 , 2 , 10 * 60 , 10 * 60 + 1 ]
178+ m_time .side_effect = [1 , 1 , 2 , 10 * 60 + 1 ]
194179 expected_msg = "Instance can't be reached after 10 minutes. Failed to obtain new boot id"
195- expected_call_args = [mock .call ("cat /proc/sys/kernel/random/boot_id" , no_log = True )] * 2
180+ expected_call_args = [mocker .call ("cat /proc/sys/kernel/random/boot_id" , no_log = True )] * 2
196181
197182 with pytest .raises (PycloudlibTimeoutError ) as excinfo :
198183 instance .wait_for_restart (old_boot_id = "11111111-1111-1111-1111-111111111111" )
@@ -207,44 +192,116 @@ def test_boot_id_failure(
207192class TestWaitForCloudinit :
208193 """Tests covering pycloudlib.instance.Instance._wait_for_cloudinit."""
209194
210- def test_with_wait_available (self , concrete_instance_cls ):
195+ def test_with_wait_available (self , concrete_instance_cls , mocker ):
211196 """Test the happy path for instances with `status --wait`."""
212197 instance = concrete_instance_cls (key_pair = None )
213- with mock .patch .object (instance , "execute" ) as m_execute :
214- instance ._wait_for_cloudinit ()
198+ m_execute = mocker .patch .object (instance , "execute" )
199+ instance ._wait_for_cloudinit ()
215200
216201 assert (
217- mock .call (
202+ mocker .call (
218203 ["cloud-init" , "status" , "--wait" , "--long" ],
219204 description = "waiting for start" ,
220205 )
221206 == m_execute .call_args
222207 )
223208
224- @mock .patch ("time.sleep" )
225- def test_wait_on_target_not_active (self , _m_sleep , concrete_instance_cls ):
209+ def test_wait_on_target_not_active (self , concrete_instance_cls , mocker ):
226210 """Test that we wait for cloud-init is-active before calling status."""
211+ mocker .patch ("time.sleep" )
227212 instance = concrete_instance_cls (key_pair = None )
228- with mock .patch .object (
213+ m_execute = mocker .patch .object (
229214 instance ,
230215 "execute" ,
231216 side_effect = [Result ("" , "" , 0 )] + [Result ("" , "" , 1 )] * 500 ,
232- ) as m_execute :
233- instance ._wait_for_cloudinit ()
217+ )
218+
219+ instance ._wait_for_cloudinit ()
234220 expected = [
235- mock .call ("command -v systemctl" ),
221+ mocker .call ("command -v systemctl" ),
236222 * (
237223 [
238- mock .call (
224+ mocker .call (
239225 ["systemctl" , "is-active" , "cloud-init.target" ],
240226 no_log = True ,
241227 )
242228 ]
243229 * 300
244230 ),
245- mock .call (
231+ mocker .call (
246232 ["cloud-init" , "status" , "--wait" , "--long" ],
247233 description = "waiting for start" ,
248234 ),
249235 ]
250236 assert expected == m_execute .call_args_list
237+
238+
239+ class TestClean :
240+ """Tests covering pycloudlib.instance.BaseInstance.clean."""
241+
242+ def test_clean_with_c_all_support (self , concrete_instance_cls , mocker ):
243+ """Test clean method when cloud-init supports -c all option."""
244+ instance = concrete_instance_cls (key_pair = None )
245+ m_execute = mocker .patch .object (
246+ instance ,
247+ "execute" ,
248+ side_effect = [
249+ Result ("" , "" , 0 ), # cloud-init clean --logs --machine-id -c all
250+ Result ("" , "" , 0 ), # rm -rf /var/log/syslog
251+ ],
252+ )
253+ instance .clean ()
254+
255+ expected_calls = [
256+ mocker .call ("sudo cloud-init clean --logs --machine-id -c all" ),
257+ mocker .call ("sudo rm -rf /var/log/syslog" ),
258+ ]
259+ assert expected_calls == m_execute .call_args_list
260+
261+ def test_clean_without_arg_support (self , concrete_instance_cls , mocker ):
262+ """Test clean method when cloud-init doesn't support -c all option."""
263+ instance = concrete_instance_cls (key_pair = None )
264+ m_execute = mocker .patch .object (
265+ instance ,
266+ "execute" ,
267+ side_effect = [
268+ Result (
269+ "" ,
270+ "cloud-init clean: error: unrecognized arguments: -c all" ,
271+ 2 ,
272+ ), # First attempt fails
273+ Result ("" , "" , 0 ), # Fallback without -c all
274+ Result ("" , "" , 0 ), # rm -rf /var/log/syslog
275+ ],
276+ )
277+ instance .clean ()
278+
279+ expected_calls = [
280+ mocker .call ("sudo cloud-init clean --logs --machine-id -c all" ),
281+ mocker .call ("sudo cloud-init clean --logs" ),
282+ mocker .call ("sudo rm -rf /var/log/syslog" ),
283+ ]
284+ assert expected_calls == m_execute .call_args_list
285+
286+ def test_clean_unexpected_error (self , concrete_instance_cls , mocker ):
287+ """Test clean method does not fallback on unexpected error."""
288+ instance = concrete_instance_cls (key_pair = None )
289+ m_execute = mocker .patch .object (
290+ instance ,
291+ "execute" ,
292+ side_effect = [
293+ Result (
294+ "" ,
295+ "cloud-init clean: error: permission denied" ,
296+ 1 ,
297+ ), # Different error
298+ Result ("" , "" , 0 ), # rm -rf /var/log/syslog
299+ ],
300+ )
301+ instance .clean ()
302+
303+ expected_calls = [
304+ mocker .call ("sudo cloud-init clean --logs --machine-id -c all" ),
305+ mocker .call ("sudo rm -rf /var/log/syslog" ),
306+ ]
307+ assert expected_calls == m_execute .call_args_list
0 commit comments