1#!/usr/bin/env vpython3 2# Copyright 2024 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import contextlib 7import datetime 8import pathlib 9import unittest 10import os 11import signal 12import socket 13import subprocess 14import sys 15import tempfile 16import time 17 18import fast_local_dev_server as server 19 20sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) 21from util import server_utils 22 23 24class RegexTest(unittest.TestCase): 25 26 def testBuildIdRegex(self): 27 self.assertRegex(server.FIRST_LOG_LINE.format(build_id='abc', outdir='PWD'), 28 server.BUILD_ID_RE) 29 30 31def sendMessage(message): 32 with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: 33 sock.settimeout(1) 34 sock.connect(server_utils.SOCKET_ADDRESS) 35 server_utils.SendMessage(sock, message) 36 37 38def pollServer(): 39 try: 40 sendMessage({'message_type': server_utils.POLL_HEARTBEAT}) 41 return True 42 except ConnectionRefusedError: 43 return False 44 45 46def shouldSkip(): 47 if os.environ.get('ALLOW_EXTERNAL_BUILD_SERVER', None): 48 return False 49 return pollServer() 50 51 52def callServer(args, check=True): 53 return subprocess.run([str(server_utils.SERVER_SCRIPT.absolute())] + args, 54 cwd=pathlib.Path(__file__).parent, 55 stdout=subprocess.PIPE, 56 stderr=subprocess.STDOUT, 57 check=check, 58 text=True, 59 timeout=3) 60 61 62@contextlib.contextmanager 63def blockingFifo(fifo_path='/tmp/.fast_local_dev_server_test.fifo'): 64 fifo_path = pathlib.Path(fifo_path) 65 try: 66 if not fifo_path.exists(): 67 os.mkfifo(fifo_path) 68 yield fifo_path 69 finally: 70 # Write to the fifo nonblocking to unblock other end. 71 try: 72 pipe = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK) 73 os.write(pipe, b'') 74 os.close(pipe) 75 except OSError: 76 # Can't open non-blocking an unconnected pipe for writing. 77 pass 78 fifo_path.unlink(missing_ok=True) 79 80 81class ServerStartedTest(unittest.TestCase): 82 build_id_counter = 0 83 task_name_counter = 0 84 85 def __init__(self, *args, **kwargs): 86 super().__init__(*args, **kwargs) 87 self._tty_path = None 88 self._build_id = None 89 90 def setUp(self): 91 if shouldSkip(): 92 self.skipTest("Cannot run test when server already running.") 93 self._process = subprocess.Popen( 94 [server_utils.SERVER_SCRIPT.absolute(), '--exit-on-idle', '--quiet'], 95 start_new_session=True, 96 cwd=pathlib.Path(__file__).parent, 97 stdout=subprocess.PIPE, 98 stderr=subprocess.STDOUT, 99 text=True) 100 # pylint: disable=unused-variable 101 for attempt in range(5): 102 if pollServer(): 103 break 104 time.sleep(0.05) 105 106 def tearDown(self): 107 self._process.terminate() 108 stdout, _ = self._process.communicate() 109 if stdout != '': 110 self.fail(f'build server should be silent but it output:\n{stdout}') 111 112 @contextlib.contextmanager 113 def _register_build(self): 114 with tempfile.NamedTemporaryFile() as f: 115 build_id = f'BUILD_ID_{ServerStartedTest.build_id_counter}' 116 os.environ['AUTONINJA_BUILD_ID'] = build_id 117 os.environ['AUTONINJA_STDOUT_NAME'] = f.name 118 ServerStartedTest.build_id_counter += 1 119 build_proc = subprocess.Popen( 120 [sys.executable, '-c', 'import time; time.sleep(100)']) 121 callServer( 122 ['--register-build', build_id, '--builder-pid', 123 str(build_proc.pid)]) 124 self._tty_path = f.name 125 self._build_id = build_id 126 try: 127 yield 128 finally: 129 self._tty_path = None 130 self._build_id = None 131 del os.environ['AUTONINJA_BUILD_ID'] 132 del os.environ['AUTONINJA_STDOUT_NAME'] 133 build_proc.kill() 134 build_proc.wait() 135 136 def sendTask(self, cmd, stamp_path=None): 137 if stamp_path: 138 _stamp_file = pathlib.Path(stamp_path) 139 else: 140 _stamp_file = pathlib.Path('/tmp/.test.stamp') 141 _stamp_file.touch() 142 143 name_prefix = f'{self._build_id}-{ServerStartedTest.task_name_counter}' 144 sendMessage({ 145 'name': f'{name_prefix}: {" ".join(cmd)}', 146 'message_type': server_utils.ADD_TASK, 147 'cmd': cmd, 148 # So that logfiles do not clutter cwd. 149 'cwd': '/tmp/', 150 'build_id': self._build_id, 151 'stamp_file': _stamp_file.name, 152 }) 153 ServerStartedTest.task_name_counter += 1 154 155 def getTtyContents(self): 156 return pathlib.Path(self._tty_path).read_text() 157 158 def getBuildInfo(self): 159 build_info = server.query_build_info(self._build_id)['builds'][0] 160 pending_tasks = build_info['pending_tasks'] 161 completed_tasks = build_info['completed_tasks'] 162 return pending_tasks, completed_tasks 163 164 def waitForTasksDone(self, timeout_seconds=3): 165 timeout_duration = datetime.timedelta(seconds=timeout_seconds) 166 start_time = datetime.datetime.now() 167 while True: 168 pending_tasks, completed_tasks = self.getBuildInfo() 169 170 if completed_tasks > 0 and pending_tasks == 0: 171 return 172 173 current_time = datetime.datetime.now() 174 duration = current_time - start_time 175 if duration > timeout_duration: 176 raise TimeoutError('Timed out waiting for pending tasks ' + 177 f'[{pending_tasks}/{pending_tasks+completed_tasks}]') 178 time.sleep(0.1) 179 180 def testRunsQuietTask(self): 181 with self._register_build(): 182 self.sendTask(['true']) 183 self.waitForTasksDone() 184 self.assertEqual(self.getTtyContents(), '') 185 186 def testRunsNoisyTask(self): 187 with self._register_build(): 188 self.sendTask(['echo', 'some_output']) 189 self.waitForTasksDone() 190 tty_contents = self.getTtyContents() 191 self.assertIn('some_output', tty_contents) 192 193 def testStampFileDeletedOnFailedTask(self): 194 with self._register_build(): 195 stamp_file = pathlib.Path('/tmp/.failed_task.stamp') 196 self.sendTask(['echo', 'some_output'], stamp_path=stamp_file) 197 self.waitForTasksDone() 198 self.assertFalse(stamp_file.exists()) 199 200 def testStampFileNotDeletedOnSuccess(self): 201 with self._register_build(): 202 stamp_file = pathlib.Path('/tmp/.successful_task.stamp') 203 self.sendTask(['true'], stamp_path=stamp_file) 204 self.waitForTasksDone() 205 self.assertTrue(stamp_file.exists()) 206 207 def testWaitForBuildServerCall(self): 208 with self._register_build(): 209 callServer(['--wait-for-build', self._build_id]) 210 self.assertEqual(self.getTtyContents(), '') 211 212 def testWaitForIdleServerCall(self): 213 with self._register_build(): 214 self.sendTask(['true']) 215 proc_result = callServer(['--wait-for-idle']) 216 self.assertIn('All', proc_result.stdout) 217 self.assertEqual('', self.getTtyContents()) 218 219 def testCancelBuildServerCall(self): 220 with self._register_build(): 221 callServer(['--cancel-build', self._build_id]) 222 self.assertEqual(self.getTtyContents(), '') 223 224 def testBuildStatusServerCall(self): 225 with self._register_build(): 226 proc_result = callServer(['--print-status', self._build_id]) 227 self.assertEqual(proc_result.stdout, '') 228 229 proc_result = callServer(['--print-status-all']) 230 self.assertIn(self._build_id, proc_result.stdout) 231 232 self.sendTask(['true']) 233 self.waitForTasksDone() 234 235 proc_result = callServer(['--print-status', self._build_id]) 236 self.assertEqual('', proc_result.stdout) 237 238 proc_result = callServer(['--print-status-all']) 239 self.assertIn('has 1 registered build', proc_result.stdout) 240 self.assertIn('[1/1]', proc_result.stdout) 241 242 with blockingFifo() as fifo_path: 243 # cat gets stuck until we open the other end of the fifo. 244 self.sendTask(['cat', str(fifo_path)]) 245 proc_result = callServer(['--print-status', self._build_id]) 246 self.assertIn('is still 1 static analysis job', proc_result.stdout) 247 self.assertIn('--wait-for-idle', proc_result.stdout) 248 proc_result = callServer(['--print-status-all']) 249 self.assertIn('[1/2]', proc_result.stdout) 250 251 self.waitForTasksDone() 252 callServer(['--cancel-build', self._build_id]) 253 self.waitForTasksDone() 254 proc_result = callServer(['--print-status', self._build_id]) 255 self.assertEqual('', proc_result.stdout) 256 257 proc_result = callServer(['--print-status-all']) 258 self.assertIn('Siso finished', proc_result.stdout) 259 260 def testServerCancelsRunningTasks(self): 261 output_stamp = pathlib.Path('/tmp/.deleteme.stamp') 262 with blockingFifo() as fifo_path: 263 self.assertFalse(output_stamp.exists()) 264 # dd blocks on fifo so task never finishes inside with block. 265 with self._register_build(): 266 self.sendTask(['dd', f'if={str(fifo_path)}', f'of={str(output_stamp)}']) 267 callServer(['--cancel-build', self._build_id]) 268 self.waitForTasksDone() 269 self.assertFalse(output_stamp.exists()) 270 271 def testKeyboardInterrupt(self): 272 os.kill(self._process.pid, signal.SIGINT) 273 self._process.wait(timeout=1) 274 275 276class ServerNotStartedTest(unittest.TestCase): 277 278 def setUp(self): 279 if pollServer(): 280 self.skipTest("Cannot run test when server already running.") 281 282 def testWaitForBuildServerCall(self): 283 proc_result = callServer(['--wait-for-build', 'invalid-build-id']) 284 self.assertIn('No server running', proc_result.stdout) 285 286 def testBuildStatusServerCall(self): 287 proc_result = callServer(['--print-status-all']) 288 self.assertIn('No server running', proc_result.stdout) 289 290 291if __name__ == '__main__': 292 # Suppress logging messages. 293 unittest.main(buffer=True) 294