| #!/usr/bin/env vpython3 |
| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import contextlib |
| import datetime |
| import pathlib |
| import unittest |
| import os |
| import signal |
| import socket |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| import fast_local_dev_server as server |
| |
| sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) |
| from util import server_utils |
| |
| |
| class RegexTest(unittest.TestCase): |
| |
| def testBuildIdRegex(self): |
| self.assertRegex(server.FIRST_LOG_LINE.format(build_id='abc', outdir='PWD'), |
| server.BUILD_ID_RE) |
| |
| |
| def sendMessage(message): |
| with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock: |
| sock.settimeout(1) |
| sock.connect(server_utils.SOCKET_ADDRESS) |
| server_utils.SendMessage(sock, message) |
| |
| |
| def pollServer(): |
| try: |
| sendMessage({'message_type': server_utils.POLL_HEARTBEAT}) |
| return True |
| except ConnectionRefusedError: |
| return False |
| |
| |
| def shouldSkip(): |
| if os.environ.get('ALLOW_EXTERNAL_BUILD_SERVER', None): |
| return False |
| return pollServer() |
| |
| |
| def callServer(args, check=True): |
| return subprocess.run([str(server_utils.SERVER_SCRIPT.absolute())] + args, |
| cwd=pathlib.Path(__file__).parent, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| check=check, |
| text=True, |
| timeout=3) |
| |
| |
| @contextlib.contextmanager |
| def blockingFifo(fifo_path='/tmp/.fast_local_dev_server_test.fifo'): |
| fifo_path = pathlib.Path(fifo_path) |
| try: |
| if not fifo_path.exists(): |
| os.mkfifo(fifo_path) |
| yield fifo_path |
| finally: |
| # Write to the fifo nonblocking to unblock other end. |
| try: |
| pipe = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK) |
| os.write(pipe, b'') |
| os.close(pipe) |
| except OSError: |
| # Can't open non-blocking an unconnected pipe for writing. |
| pass |
| fifo_path.unlink(missing_ok=True) |
| |
| |
| class ServerStartedTest(unittest.TestCase): |
| build_id_counter = 0 |
| task_name_counter = 0 |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self._tty_path = None |
| self._build_id = None |
| |
| def setUp(self): |
| if shouldSkip(): |
| self.skipTest("Cannot run test when server already running.") |
| self._process = subprocess.Popen( |
| [server_utils.SERVER_SCRIPT.absolute(), '--exit-on-idle', '--quiet'], |
| start_new_session=True, |
| cwd=pathlib.Path(__file__).parent, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True) |
| # pylint: disable=unused-variable |
| for attempt in range(5): |
| if pollServer(): |
| break |
| time.sleep(0.05) |
| |
| def tearDown(self): |
| self._process.terminate() |
| stdout, _ = self._process.communicate() |
| if stdout != '': |
| self.fail(f'build server should be silent but it output:\n{stdout}') |
| |
| @contextlib.contextmanager |
| def _register_build(self): |
| with tempfile.NamedTemporaryFile() as f: |
| build_id = f'BUILD_ID_{ServerStartedTest.build_id_counter}' |
| os.environ['AUTONINJA_BUILD_ID'] = build_id |
| os.environ['AUTONINJA_STDOUT_NAME'] = f.name |
| ServerStartedTest.build_id_counter += 1 |
| build_proc = subprocess.Popen( |
| [sys.executable, '-c', 'import time; time.sleep(100)']) |
| callServer( |
| ['--register-build', build_id, '--builder-pid', |
| str(build_proc.pid)]) |
| self._tty_path = f.name |
| self._build_id = build_id |
| try: |
| yield |
| finally: |
| self._tty_path = None |
| self._build_id = None |
| del os.environ['AUTONINJA_BUILD_ID'] |
| del os.environ['AUTONINJA_STDOUT_NAME'] |
| build_proc.kill() |
| build_proc.wait() |
| |
| def sendTask(self, cmd, stamp_path=None): |
| if stamp_path: |
| _stamp_file = pathlib.Path(stamp_path) |
| else: |
| _stamp_file = pathlib.Path('/tmp/.test.stamp') |
| _stamp_file.touch() |
| |
| name_prefix = f'{self._build_id}-{ServerStartedTest.task_name_counter}' |
| sendMessage({ |
| 'name': f'{name_prefix}: {" ".join(cmd)}', |
| 'message_type': server_utils.ADD_TASK, |
| 'cmd': cmd, |
| # So that logfiles do not clutter cwd. |
| 'cwd': '/tmp/', |
| 'build_id': self._build_id, |
| 'stamp_file': _stamp_file.name, |
| }) |
| ServerStartedTest.task_name_counter += 1 |
| |
| def getTtyContents(self): |
| return pathlib.Path(self._tty_path).read_text() |
| |
| def getBuildInfo(self): |
| build_info = server.query_build_info(self._build_id)['builds'][0] |
| pending_tasks = build_info['pending_tasks'] |
| completed_tasks = build_info['completed_tasks'] |
| return pending_tasks, completed_tasks |
| |
| def waitForTasksDone(self, timeout_seconds=3): |
| timeout_duration = datetime.timedelta(seconds=timeout_seconds) |
| start_time = datetime.datetime.now() |
| while True: |
| pending_tasks, completed_tasks = self.getBuildInfo() |
| |
| if completed_tasks > 0 and pending_tasks == 0: |
| return |
| |
| current_time = datetime.datetime.now() |
| duration = current_time - start_time |
| if duration > timeout_duration: |
| raise TimeoutError('Timed out waiting for pending tasks ' + |
| f'[{pending_tasks}/{pending_tasks+completed_tasks}]') |
| time.sleep(0.1) |
| |
| def testRunsQuietTask(self): |
| with self._register_build(): |
| self.sendTask(['true']) |
| self.waitForTasksDone() |
| self.assertEqual(self.getTtyContents(), '') |
| |
| def testRunsNoisyTask(self): |
| with self._register_build(): |
| self.sendTask(['echo', 'some_output']) |
| self.waitForTasksDone() |
| tty_contents = self.getTtyContents() |
| self.assertIn('some_output', tty_contents) |
| |
| def testStampFileDeletedOnFailedTask(self): |
| with self._register_build(): |
| stamp_file = pathlib.Path('/tmp/.failed_task.stamp') |
| self.sendTask(['echo', 'some_output'], stamp_path=stamp_file) |
| self.waitForTasksDone() |
| self.assertFalse(stamp_file.exists()) |
| |
| def testStampFileNotDeletedOnSuccess(self): |
| with self._register_build(): |
| stamp_file = pathlib.Path('/tmp/.successful_task.stamp') |
| self.sendTask(['true'], stamp_path=stamp_file) |
| self.waitForTasksDone() |
| self.assertTrue(stamp_file.exists()) |
| |
| def testWaitForBuildServerCall(self): |
| with self._register_build(): |
| callServer(['--wait-for-build', self._build_id]) |
| self.assertEqual(self.getTtyContents(), '') |
| |
| def testWaitForIdleServerCall(self): |
| with self._register_build(): |
| self.sendTask(['true']) |
| proc_result = callServer(['--wait-for-idle']) |
| self.assertIn('All', proc_result.stdout) |
| self.assertEqual('', self.getTtyContents()) |
| |
| def testCancelBuildServerCall(self): |
| with self._register_build(): |
| callServer(['--cancel-build', self._build_id]) |
| self.assertEqual(self.getTtyContents(), '') |
| |
| def testBuildStatusServerCall(self): |
| with self._register_build(): |
| proc_result = callServer(['--print-status', self._build_id]) |
| self.assertEqual(proc_result.stdout, '') |
| |
| proc_result = callServer(['--print-status-all']) |
| self.assertIn(self._build_id, proc_result.stdout) |
| |
| self.sendTask(['true']) |
| self.waitForTasksDone() |
| |
| proc_result = callServer(['--print-status', self._build_id]) |
| self.assertEqual('', proc_result.stdout) |
| |
| proc_result = callServer(['--print-status-all']) |
| self.assertIn('has 1 registered build', proc_result.stdout) |
| self.assertIn('1/1', proc_result.stdout) |
| |
| with blockingFifo() as fifo_path: |
| # cat gets stuck until we open the other end of the fifo. |
| self.sendTask(['cat', str(fifo_path)]) |
| proc_result = callServer(['--print-status', self._build_id]) |
| self.assertIn('is still 1 static analysis job', proc_result.stdout) |
| self.assertIn('--wait-for-idle', proc_result.stdout) |
| proc_result = callServer(['--print-status-all']) |
| self.assertIn('1/2', proc_result.stdout) |
| |
| self.waitForTasksDone() |
| callServer(['--cancel-build', self._build_id]) |
| self.waitForTasksDone() |
| proc_result = callServer(['--print-status', self._build_id]) |
| self.assertEqual('', proc_result.stdout) |
| |
| proc_result = callServer(['--print-status-all']) |
| self.assertIn('Main build completed', proc_result.stdout) |
| |
| def testServerCancelsRunningTasks(self): |
| output_stamp = pathlib.Path('/tmp/.deleteme.stamp') |
| with blockingFifo() as fifo_path: |
| self.assertFalse(output_stamp.exists()) |
| # dd blocks on fifo so task never finishes inside with block. |
| with self._register_build(): |
| self.sendTask(['dd', f'if={str(fifo_path)}', f'of={str(output_stamp)}']) |
| callServer(['--cancel-build', self._build_id]) |
| self.waitForTasksDone() |
| self.assertFalse(output_stamp.exists()) |
| |
| def testKeyboardInterrupt(self): |
| os.kill(self._process.pid, signal.SIGINT) |
| self._process.wait(timeout=1) |
| |
| |
| class ServerNotStartedTest(unittest.TestCase): |
| |
| def setUp(self): |
| if pollServer(): |
| self.skipTest("Cannot run test when server already running.") |
| |
| def testWaitForBuildServerCall(self): |
| proc_result = callServer(['--wait-for-build', 'invalid-build-id']) |
| self.assertIn('No server running', proc_result.stdout) |
| |
| def testBuildStatusServerCall(self): |
| proc_result = callServer(['--print-status-all']) |
| self.assertIn('No server running', proc_result.stdout) |
| |
| |
| if __name__ == '__main__': |
| # Suppress logging messages. |
| unittest.main(buffer=True) |