1#!/usr/bin/env python3 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16import unittest 17 18import mock 19import subprocess 20from acts.libs.proc.process import Process 21from acts.libs.proc.process import ProcessError 22 23class FakeThread(object): 24 def __init__(self, target=None): 25 self.target = target 26 self.alive = False 27 28 def _on_start(self): 29 pass 30 31 def start(self): 32 self.alive = True 33 if self._on_start: 34 self._on_start() 35 36 def stop(self): 37 self.alive = False 38 39 def join(self): 40 pass 41 42 43class ProcessTest(unittest.TestCase): 44 """Tests the acts.libs.proc.process.Process class.""" 45 46 def setUp(self): 47 self._Process__start_process = Process._Process__start_process 48 49 def tearDown(self): 50 Process._Process__start_process = self._Process__start_process 51 52 @staticmethod 53 def patch(imported_name, *args, **kwargs): 54 return mock.patch('acts.libs.proc.process.%s' % imported_name, 55 *args, **kwargs) 56 57 # set_on_output_callback 58 59 def test_set_on_output_callback(self): 60 """Tests that set_on_output_callback sets on_output_callback.""" 61 callback = mock.Mock() 62 63 process = Process('cmd').set_on_output_callback(callback) 64 process._on_output_callback() 65 66 self.assertTrue(callback.called) 67 68 # set_on_terminate_callback 69 70 def test_set_on_terminate_callback(self): 71 """Tests that set_on_terminate_callback sets _on_terminate_callback.""" 72 callback = mock.Mock() 73 74 process = Process('cmd').set_on_terminate_callback(callback) 75 process._on_terminate_callback() 76 77 self.assertTrue(callback.called) 78 79 # start 80 81 def test_start_raises_if_called_back_to_back(self): 82 """Tests that start raises an exception if it has already been called 83 prior. 84 85 This is required to prevent references to processes and threads from 86 being overwritten, potentially causing ACTS to hang.""" 87 process = Process('cmd') 88 89 # Here we need the thread to start the process object. 90 class FakeThreadImpl(FakeThread): 91 def _on_start(self): 92 process._process = mock.Mock() 93 94 with self.patch('Thread', FakeThreadImpl): 95 process.start() 96 expected_msg = 'Process has already started.' 97 with self.assertRaisesRegex(ProcessError, expected_msg): 98 process.start() 99 100 def test_start_starts_listening_thread(self): 101 """Tests that start starts the _exec_popen_loop function.""" 102 process = Process('cmd') 103 104 # Here we need the thread to start the process object. 105 class FakeThreadImpl(FakeThread): 106 def _on_start(self): 107 process._process = mock.Mock() 108 109 with self.patch('Thread', FakeThreadImpl): 110 process.start() 111 112 self.assertTrue(process._listening_thread.alive) 113 self.assertEqual(process._listening_thread.target, process._exec_loop) 114 115 # wait 116 117 def test_wait_raises_if_called_back_to_back(self): 118 """Tests that wait raises an exception if it has already been called 119 prior.""" 120 process = Process('cmd') 121 process._process = mock.Mock() 122 123 process.wait(0) 124 expected_msg = 'Process is already being stopped.' 125 with self.assertRaisesRegex(ProcessError, expected_msg): 126 process.wait(0) 127 128 @mock.patch.object(Process, '_kill_process') 129 def test_wait_kills_after_timeout(self, *_): 130 """Tests that if a TimeoutExpired error is thrown during wait, the 131 process is killed.""" 132 process = Process('cmd') 133 process._process = mock.Mock() 134 process._process.wait.side_effect = subprocess.TimeoutExpired('', '') 135 136 process.wait(0) 137 138 self.assertEqual(process._kill_process.called, True) 139 140 @mock.patch.object(Process, '_kill_process') 141 def test_wait_sets_stopped_to_true_before_process_kill(self, *_): 142 """Tests that stop() sets the _stopped attribute to True. 143 144 This order is required to prevent the _exec_loop from calling 145 _on_terminate_callback when the user has killed the process. 146 """ 147 verifier = mock.Mock() 148 verifier.passed = False 149 150 def test_call_order(): 151 self.assertTrue(process._stopped) 152 verifier.passed = True 153 154 process = Process('cmd') 155 process._process = mock.Mock() 156 process._process.poll.return_value = None 157 process._process.wait.side_effect = subprocess.TimeoutExpired('', '') 158 process._kill_process = test_call_order 159 160 process.wait() 161 162 self.assertEqual(verifier.passed, True) 163 164 def test_wait_joins_listening_thread_if_it_exists(self): 165 """Tests wait() joins _listening_thread if it exists.""" 166 process = Process('cmd') 167 process._process = mock.Mock() 168 mocked_thread = mock.Mock() 169 process._listening_thread = mocked_thread 170 171 process.wait(0) 172 173 self.assertEqual(mocked_thread.join.called, True) 174 175 def test_wait_clears_listening_thread_if_it_exists(self): 176 """Tests wait() joins _listening_thread if it exists. 177 178 Threads can only be started once, so after wait has been called, we 179 want to make sure we clear the listening thread. 180 """ 181 process = Process('cmd') 182 process._process = mock.Mock() 183 process._listening_thread = mock.Mock() 184 185 process.wait(0) 186 187 self.assertEqual(process._listening_thread, None) 188 189 def test_wait_joins_redirection_thread_if_it_exists(self): 190 """Tests wait() joins _listening_thread if it exists.""" 191 process = Process('cmd') 192 process._process = mock.Mock() 193 mocked_thread = mock.Mock() 194 process._redirection_thread = mocked_thread 195 196 process.wait(0) 197 198 self.assertEqual(mocked_thread.join.called, True) 199 200 def test_wait_clears_redirection_thread_if_it_exists(self): 201 """Tests wait() joins _listening_thread if it exists. 202 203 Threads can only be started once, so after wait has been called, we 204 want to make sure we clear the listening thread. 205 """ 206 process = Process('cmd') 207 process._process = mock.Mock() 208 process._redirection_thread = mock.Mock() 209 210 process.wait(0) 211 212 self.assertEqual(process._redirection_thread, None) 213 214 # stop 215 216 def test_stop_sets_stopped_to_true(self): 217 """Tests that stop() sets the _stopped attribute to True.""" 218 process = Process('cmd') 219 process._process = mock.Mock() 220 221 process.stop() 222 223 self.assertTrue(process._stopped) 224 225 def test_stop_sets_stopped_to_true_before_process_kill(self): 226 """Tests that stop() sets the _stopped attribute to True. 227 228 This order is required to prevent the _exec_loop from calling 229 _on_terminate_callback when the user has killed the process. 230 """ 231 verifier = mock.Mock() 232 verifier.passed = False 233 234 def test_call_order(): 235 self.assertTrue(process._stopped) 236 verifier.passed = True 237 238 process = Process('cmd') 239 process._process = mock.Mock() 240 process._process.poll.return_value = None 241 process._kill_process = test_call_order 242 process._process.wait.side_effect = subprocess.TimeoutExpired('', '') 243 244 process.stop() 245 246 self.assertEqual(verifier.passed, True) 247 248 def test_stop_calls_wait(self): 249 """Tests that stop() also has the functionality of wait().""" 250 process = Process('cmd') 251 process._process = mock.Mock() 252 process.wait = mock.Mock() 253 254 process.stop() 255 256 self.assertEqual(process.wait.called, True) 257 258 # _redirect_output 259 260 def test_redirect_output_feeds_all_lines_to_on_output_callback(self): 261 """Tests that _redirect_output loops until all lines are parsed.""" 262 received_list = [] 263 264 def appender(line): 265 received_list.append(line) 266 267 process = Process('cmd') 268 process.set_on_output_callback(appender) 269 process._process = mock.Mock() 270 process._process.stdout.readline.side_effect = [b'a\n', b'b\n', b''] 271 272 process._redirect_output() 273 274 self.assertEqual(received_list[0], 'a') 275 self.assertEqual(received_list[1], 'b') 276 self.assertEqual(len(received_list), 2) 277 278 # __start_process 279 280 def test_start_process_returns_a_popen_object(self): 281 """Tests that a Popen object is returned by __start_process.""" 282 with self.patch('subprocess.Popen', return_value='verification'): 283 self.assertEqual(Process._Process__start_process('cmd'), 284 'verification') 285 286 # _exec_loop 287 288 def test_exec_loop_redirections_output(self): 289 """Tests that the _exec_loop function calls to redirect the output.""" 290 process = Process('cmd') 291 Process._Process__start_process = mock.Mock() 292 293 with self.patch('Thread', FakeThread): 294 process._exec_loop() 295 296 self.assertEqual(process._redirection_thread.target, 297 process._redirect_output) 298 self.assertEqual(process._redirection_thread.alive, True) 299 300 def test_exec_loop_waits_for_process(self): 301 """Tests that the _exec_loop waits for the process to complete before 302 returning.""" 303 process = Process('cmd') 304 Process._Process__start_process = mock.Mock() 305 306 with self.patch('Thread', FakeThread): 307 process._exec_loop() 308 309 self.assertEqual(process._process.wait.called, True) 310 311 def test_exec_loop_loops_if_not_stopped(self): 312 process = Process('1st') 313 Process._Process__start_process = mock.Mock() 314 process._on_terminate_callback = mock.Mock(side_effect=[['2nd'], None]) 315 316 with self.patch('Thread', FakeThread): 317 process._exec_loop() 318 319 self.assertEqual(Process._Process__start_process.call_count, 2) 320 self.assertEqual(Process._Process__start_process.call_args_list[0][0], 321 (['1st'], )) 322 self.assertEqual(Process._Process__start_process.call_args_list[1][0], 323 (['2nd'], )) 324 325 def test_exec_loop_does_not_loop_if_stopped(self): 326 process = Process('1st') 327 Process._Process__start_process = mock.Mock() 328 process._on_terminate_callback = mock.Mock( 329 side_effect=['2nd', None]) 330 process._stopped = True 331 332 with self.patch('Thread', FakeThread): 333 process._exec_loop() 334 335 self.assertEqual(Process._Process__start_process.call_count, 1) 336 self.assertEqual( 337 Process._Process__start_process.call_args_list[0][0], 338 (['1st'],)) 339 340 341if __name__ == '__main__': 342 unittest.main() 343