Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 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 | |
| 6 | import contextlib |
| 7 | import datetime |
| 8 | import json |
| 9 | import pathlib |
| 10 | import unittest |
| 11 | import os |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 12 | import signal |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 13 | import socket |
| 14 | import subprocess |
| 15 | import sys |
| 16 | import time |
| 17 | import uuid |
| 18 | |
| 19 | import fast_local_dev_server as server |
| 20 | |
| 21 | sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) |
| 22 | from util import server_utils |
| 23 | |
| 24 | |
| 25 | class RegexTest(unittest.TestCase): |
| 26 | |
| 27 | def testBuildIdRegex(self): |
| 28 | self.assertRegex(server.FIRST_LOG_LINE.format(build_id='abc'), |
| 29 | server.BUILD_ID_RE) |
| 30 | |
| 31 | |
| 32 | def sendMessage(message_dict): |
| 33 | with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: |
| 34 | sock.settimeout(1) |
| 35 | sock.connect(server_utils.SOCKET_ADDRESS) |
| 36 | server_utils.SendMessage(sock, json.dumps(message_dict).encode('utf-8')) |
| 37 | |
| 38 | |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 39 | def pollServer(): |
| 40 | try: |
| 41 | sendMessage({'message_type': server_utils.POLL_HEARTBEAT}) |
| 42 | return True |
| 43 | except ConnectionRefusedError: |
| 44 | return False |
| 45 | |
| 46 | |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 47 | def callServer(args, stdout=subprocess.DEVNULL, check=True): |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 48 | return subprocess.run([str(server_utils.SERVER_SCRIPT.absolute())] + args, |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 49 | cwd=pathlib.Path(__file__).parent, |
| 50 | stdout=stdout, |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 51 | stderr=subprocess.STDOUT, |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 52 | check=check, |
| 53 | text=True) |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 54 | |
| 55 | |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 56 | @contextlib.contextmanager |
| 57 | def blockingFifo(fifo_path='/tmp/.fast_local_dev_server_test.fifo'): |
| 58 | fifo_path = pathlib.Path(fifo_path) |
| 59 | try: |
| 60 | if not fifo_path.exists(): |
| 61 | os.mkfifo(fifo_path) |
| 62 | yield fifo_path |
| 63 | finally: |
| 64 | # Write to the fifo nonblocking to unblock other end. |
| 65 | try: |
| 66 | pipe = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK) |
| 67 | os.write(pipe, b'') |
| 68 | os.close(pipe) |
| 69 | except OSError: |
| 70 | # Can't open non-blocking an unconnected pipe for writing. |
| 71 | pass |
| 72 | fifo_path.unlink(missing_ok=True) |
| 73 | |
| 74 | |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 75 | class TasksTest(unittest.TestCase): |
| 76 | |
| 77 | def setUp(self): |
| 78 | self._TTY_FILE = '/tmp/fast_local_dev_server_test_tty' |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 79 | if pollServer(): |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 80 | # TODO(mheikal): Support overriding the standard named pipe for |
| 81 | # communicating with the server so that we can run an instance just for |
| 82 | # this test even if a real one is running. |
| 83 | self.skipTest("Cannot run test when server already running.") |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 84 | self._process = subprocess.Popen( |
| 85 | [server_utils.SERVER_SCRIPT.absolute(), '--exit-on-idle', '--quiet'], |
| 86 | start_new_session=True, |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 87 | cwd=pathlib.Path(__file__).parent, |
| 88 | stdout=subprocess.PIPE, |
| 89 | stderr=subprocess.STDOUT, |
| 90 | text=True) |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 91 | # pylint: disable=unused-variable |
| 92 | for attempt in range(5): |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 93 | if pollServer(): |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 94 | break |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 95 | time.sleep(0.05) |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 96 | |
| 97 | def tearDown(self): |
| 98 | if os.path.exists(self._TTY_FILE): |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 99 | os.unlink(self._TTY_FILE) |
| 100 | self._process.terminate() |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 101 | stdout, _ = self._process.communicate() |
| 102 | if stdout != '': |
| 103 | self.fail(f'build server should be silent but it output:\n{stdout}') |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 104 | |
Mohamed Heikal | 9984e43 | 2024-12-03 18:21:40 | [diff] [blame] | 105 | def sendTask(self, cmd, stamp_path=None): |
| 106 | if stamp_path: |
| 107 | _stamp_file = pathlib.Path(stamp_path) |
| 108 | else: |
| 109 | _stamp_file = pathlib.Path('/tmp/.test.stamp') |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 110 | _stamp_file.touch() |
Mohamed Heikal | 9984e43 | 2024-12-03 18:21:40 | [diff] [blame] | 111 | |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 112 | sendMessage({ |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 113 | 'name': f'{self.id()}({uuid.uuid4()}): {" ".join(cmd)}', |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 114 | 'message_type': server_utils.ADD_TASK, |
| 115 | 'cmd': cmd, |
| 116 | # So that logfiles do not clutter cwd. |
| 117 | 'cwd': '/tmp/', |
| 118 | 'tty': self._TTY_FILE, |
| 119 | 'build_id': self.id(), |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 120 | 'stamp_file': _stamp_file.name, |
| 121 | }) |
| 122 | |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 123 | def getTtyContents(self): |
| 124 | if os.path.exists(self._TTY_FILE): |
| 125 | with open(self._TTY_FILE, 'rt') as tty: |
| 126 | return tty.read() |
| 127 | return '' |
| 128 | |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 129 | def getBuildInfo(self): |
| 130 | build_info = server.query_build_info(self.id()) |
| 131 | pending_tasks = build_info['pending_tasks'] |
| 132 | completed_tasks = build_info['completed_tasks'] |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 133 | return pending_tasks, completed_tasks |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 134 | |
| 135 | def waitForTasksDone(self, timeout_seconds=3): |
| 136 | timeout_duration = datetime.timedelta(seconds=timeout_seconds) |
| 137 | start_time = datetime.datetime.now() |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 138 | while True: |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 139 | pending_tasks, completed_tasks = self.getBuildInfo() |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 140 | |
| 141 | if completed_tasks > 0 and pending_tasks == 0: |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 142 | return |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 143 | |
| 144 | current_time = datetime.datetime.now() |
| 145 | duration = current_time - start_time |
| 146 | if duration > timeout_duration: |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 147 | raise TimeoutError( |
| 148 | f'Timed out waiting for pending tasks [{pending_tasks}/{pending_tasks+completed_tasks}]' |
| 149 | ) |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 150 | time.sleep(0.1) |
| 151 | |
| 152 | def testRunsQuietTask(self): |
| 153 | self.sendTask(['true']) |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 154 | self.waitForTasksDone() |
| 155 | self.assertEqual(self.getTtyContents(), '') |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 156 | |
| 157 | def testRunsNoisyTask(self): |
| 158 | self.sendTask(['echo', 'some_output']) |
Mohamed Heikal | ee1abc2 | 2024-11-14 21:50:31 | [diff] [blame] | 159 | self.waitForTasksDone() |
| 160 | tty_contents = self.getTtyContents() |
| 161 | self.assertIn('some_output', tty_contents) |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 162 | |
Mohamed Heikal | 9984e43 | 2024-12-03 18:21:40 | [diff] [blame] | 163 | def testStampFileDeletedOnFailedTask(self): |
| 164 | stamp_file = pathlib.Path('/tmp/.failed_task.stamp') |
| 165 | self.sendTask(['echo', 'some_output'], stamp_path=stamp_file) |
| 166 | self.waitForTasksDone() |
| 167 | self.assertFalse(stamp_file.exists()) |
| 168 | |
| 169 | def testStampFileNotDeletedOnSuccess(self): |
| 170 | stamp_file = pathlib.Path('/tmp/.successful_task.stamp') |
| 171 | self.sendTask(['true'], stamp_path=stamp_file) |
| 172 | self.waitForTasksDone() |
| 173 | self.assertTrue(stamp_file.exists()) |
| 174 | |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 175 | def testRegisterBuilderMessage(self): |
| 176 | sendMessage({ |
| 177 | 'message_type': server_utils.REGISTER_BUILDER, |
| 178 | 'build_id': self.id(), |
| 179 | 'builder_pid': os.getpid(), |
| 180 | }) |
| 181 | pollServer() |
| 182 | self.assertEqual(self.getTtyContents(), '') |
| 183 | |
| 184 | def testRegisterBuilderServerCall(self): |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 185 | callServer( |
| 186 | ['--register-build', |
| 187 | self.id(), '--builder-pid', |
| 188 | str(os.getpid())]) |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 189 | self.assertEqual(self.getTtyContents(), '') |
| 190 | |
| 191 | def testWaitForBuildServerCall(self): |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 192 | callServer(['--wait-for-build', self.id()]) |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 193 | self.assertEqual(self.getTtyContents(), '') |
| 194 | |
| 195 | def testCancelBuildServerCall(self): |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 196 | callServer(['--cancel-build', self.id()]) |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 197 | self.assertEqual(self.getTtyContents(), '') |
| 198 | |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 199 | def testBuildStatusServerCall(self): |
| 200 | proc_result = callServer(['--print-status', self.id()], |
| 201 | stdout=subprocess.PIPE) |
| 202 | self.assertEqual(proc_result.stdout, '') |
| 203 | |
| 204 | self.sendTask(['true']) |
| 205 | self.waitForTasksDone() |
| 206 | proc_result = callServer(['--print-status', self.id()], |
| 207 | stdout=subprocess.PIPE) |
| 208 | self.assertIn('[1/1]', proc_result.stdout) |
| 209 | |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 210 | with blockingFifo() as fifo_path: |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 211 | # cat gets stuck until we open the other end of the fifo. |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 212 | self.sendTask(['cat', str(fifo_path)]) |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 213 | proc_result = callServer(['--print-status', self.id()], |
| 214 | stdout=subprocess.PIPE) |
| 215 | self.assertIn('[1/2]', proc_result.stdout) |
| 216 | self.assertIn(f'--wait-for-build {self.id()}', proc_result.stdout) |
| 217 | |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 218 | self.waitForTasksDone() |
Mohamed Heikal | 6b56cf6 | 2024-12-10 23:14:55 | [diff] [blame] | 219 | proc_result = callServer(['--print-status', self.id()], |
| 220 | stdout=subprocess.PIPE) |
| 221 | self.assertIn('[2/2]', proc_result.stdout) |
| 222 | |
Mohamed Heikal | abf646e | 2024-12-12 16:06:05 | [diff] [blame^] | 223 | def testServerCancelsRunningTasks(self): |
| 224 | output_stamp = pathlib.Path('/tmp/.deleteme.stamp') |
| 225 | with blockingFifo() as fifo_path: |
| 226 | self.assertFalse(output_stamp.exists()) |
| 227 | # dd blocks on fifo so task never finishes inside with block. |
| 228 | self.sendTask(['dd', f'if={str(fifo_path)}', f'of={str(output_stamp)}']) |
| 229 | callServer(['--cancel-build', self.id()]) |
| 230 | self.waitForTasksDone() |
| 231 | self.assertFalse(output_stamp.exists()) |
| 232 | |
Mohamed Heikal | b752b77 | 2024-11-25 23:05:44 | [diff] [blame] | 233 | def testKeyboardInterrupt(self): |
| 234 | os.kill(self._process.pid, signal.SIGINT) |
| 235 | self._process.wait(timeout=1) |
| 236 | |
Mohamed Heikal | f746b57f | 2024-11-13 21:20:17 | [diff] [blame] | 237 | |
| 238 | if __name__ == '__main__': |
| 239 | # Suppress logging messages. |
| 240 | unittest.main(buffer=True) |