blob: a2a13d62cf08134e1033520d991de34fae59dffe [file] [log] [blame]
Mohamed Heikalf746b57f2024-11-13 21:20:171#!/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 json
9import pathlib
10import unittest
11import os
Mohamed Heikalb752b772024-11-25 23:05:4412import signal
Mohamed Heikalf746b57f2024-11-13 21:20:1713import socket
14import subprocess
15import sys
16import time
17import uuid
18
19import fast_local_dev_server as server
20
21sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp'))
22from util import server_utils
23
24
25class 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
32def 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 Heikalb752b772024-11-25 23:05:4439def pollServer():
40 try:
41 sendMessage({'message_type': server_utils.POLL_HEARTBEAT})
42 return True
43 except ConnectionRefusedError:
44 return False
45
46
Mohamed Heikal6b56cf62024-12-10 23:14:5547def callServer(args, stdout=subprocess.DEVNULL, check=True):
Mohamed Heikalabf646e2024-12-12 16:06:0548 return subprocess.run([str(server_utils.SERVER_SCRIPT.absolute())] + args,
Mohamed Heikal6b56cf62024-12-10 23:14:5549 cwd=pathlib.Path(__file__).parent,
50 stdout=stdout,
Mohamed Heikalabf646e2024-12-12 16:06:0551 stderr=subprocess.STDOUT,
Mohamed Heikal6b56cf62024-12-10 23:14:5552 check=check,
53 text=True)
Mohamed Heikalb752b772024-11-25 23:05:4454
55
Mohamed Heikalabf646e2024-12-12 16:06:0556@contextlib.contextmanager
57def 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 Heikalf746b57f2024-11-13 21:20:1775class TasksTest(unittest.TestCase):
76
77 def setUp(self):
78 self._TTY_FILE = '/tmp/fast_local_dev_server_test_tty'
Mohamed Heikalb752b772024-11-25 23:05:4479 if pollServer():
Mohamed Heikalf746b57f2024-11-13 21:20:1780 # 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 Heikalf746b57f2024-11-13 21:20:1784 self._process = subprocess.Popen(
85 [server_utils.SERVER_SCRIPT.absolute(), '--exit-on-idle', '--quiet'],
86 start_new_session=True,
Mohamed Heikalb752b772024-11-25 23:05:4487 cwd=pathlib.Path(__file__).parent,
88 stdout=subprocess.PIPE,
89 stderr=subprocess.STDOUT,
90 text=True)
Mohamed Heikalf746b57f2024-11-13 21:20:1791 # pylint: disable=unused-variable
92 for attempt in range(5):
Mohamed Heikalb752b772024-11-25 23:05:4493 if pollServer():
Mohamed Heikalf746b57f2024-11-13 21:20:1794 break
Mohamed Heikalb752b772024-11-25 23:05:4495 time.sleep(0.05)
Mohamed Heikalf746b57f2024-11-13 21:20:1796
97 def tearDown(self):
98 if os.path.exists(self._TTY_FILE):
Mohamed Heikalf746b57f2024-11-13 21:20:1799 os.unlink(self._TTY_FILE)
100 self._process.terminate()
Mohamed Heikalb752b772024-11-25 23:05:44101 stdout, _ = self._process.communicate()
102 if stdout != '':
103 self.fail(f'build server should be silent but it output:\n{stdout}')
Mohamed Heikalf746b57f2024-11-13 21:20:17104
Mohamed Heikal9984e432024-12-03 18:21:40105 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 Heikalf746b57f2024-11-13 21:20:17110 _stamp_file.touch()
Mohamed Heikal9984e432024-12-03 18:21:40111
Mohamed Heikalf746b57f2024-11-13 21:20:17112 sendMessage({
Mohamed Heikalabf646e2024-12-12 16:06:05113 'name': f'{self.id()}({uuid.uuid4()}): {" ".join(cmd)}',
Mohamed Heikalf746b57f2024-11-13 21:20:17114 '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 Heikalf746b57f2024-11-13 21:20:17120 'stamp_file': _stamp_file.name,
121 })
122
Mohamed Heikalee1abc22024-11-14 21:50:31123 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 Heikalf746b57f2024-11-13 21:20:17129 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 Heikalee1abc22024-11-14 21:50:31133 return pending_tasks, completed_tasks
Mohamed Heikalf746b57f2024-11-13 21:20:17134
135 def waitForTasksDone(self, timeout_seconds=3):
136 timeout_duration = datetime.timedelta(seconds=timeout_seconds)
137 start_time = datetime.datetime.now()
Mohamed Heikalf746b57f2024-11-13 21:20:17138 while True:
Mohamed Heikalee1abc22024-11-14 21:50:31139 pending_tasks, completed_tasks = self.getBuildInfo()
Mohamed Heikalf746b57f2024-11-13 21:20:17140
141 if completed_tasks > 0 and pending_tasks == 0:
Mohamed Heikalee1abc22024-11-14 21:50:31142 return
Mohamed Heikalf746b57f2024-11-13 21:20:17143
144 current_time = datetime.datetime.now()
145 duration = current_time - start_time
146 if duration > timeout_duration:
Mohamed Heikalabf646e2024-12-12 16:06:05147 raise TimeoutError(
148 f'Timed out waiting for pending tasks [{pending_tasks}/{pending_tasks+completed_tasks}]'
149 )
Mohamed Heikalf746b57f2024-11-13 21:20:17150 time.sleep(0.1)
151
152 def testRunsQuietTask(self):
153 self.sendTask(['true'])
Mohamed Heikalee1abc22024-11-14 21:50:31154 self.waitForTasksDone()
155 self.assertEqual(self.getTtyContents(), '')
Mohamed Heikalf746b57f2024-11-13 21:20:17156
157 def testRunsNoisyTask(self):
158 self.sendTask(['echo', 'some_output'])
Mohamed Heikalee1abc22024-11-14 21:50:31159 self.waitForTasksDone()
160 tty_contents = self.getTtyContents()
161 self.assertIn('some_output', tty_contents)
Mohamed Heikalf746b57f2024-11-13 21:20:17162
Mohamed Heikal9984e432024-12-03 18:21:40163 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 Heikalb752b772024-11-25 23:05:44175 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 Heikal6b56cf62024-12-10 23:14:55185 callServer(
186 ['--register-build',
187 self.id(), '--builder-pid',
188 str(os.getpid())])
Mohamed Heikalb752b772024-11-25 23:05:44189 self.assertEqual(self.getTtyContents(), '')
190
191 def testWaitForBuildServerCall(self):
Mohamed Heikal6b56cf62024-12-10 23:14:55192 callServer(['--wait-for-build', self.id()])
Mohamed Heikalb752b772024-11-25 23:05:44193 self.assertEqual(self.getTtyContents(), '')
194
195 def testCancelBuildServerCall(self):
Mohamed Heikal6b56cf62024-12-10 23:14:55196 callServer(['--cancel-build', self.id()])
Mohamed Heikalb752b772024-11-25 23:05:44197 self.assertEqual(self.getTtyContents(), '')
198
Mohamed Heikal6b56cf62024-12-10 23:14:55199 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 Heikalabf646e2024-12-12 16:06:05210 with blockingFifo() as fifo_path:
Mohamed Heikal6b56cf62024-12-10 23:14:55211 # cat gets stuck until we open the other end of the fifo.
Mohamed Heikalabf646e2024-12-12 16:06:05212 self.sendTask(['cat', str(fifo_path)])
Mohamed Heikal6b56cf62024-12-10 23:14:55213 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 Heikalabf646e2024-12-12 16:06:05218 self.waitForTasksDone()
Mohamed Heikal6b56cf62024-12-10 23:14:55219 proc_result = callServer(['--print-status', self.id()],
220 stdout=subprocess.PIPE)
221 self.assertIn('[2/2]', proc_result.stdout)
222
Mohamed Heikalabf646e2024-12-12 16:06:05223 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 Heikalb752b772024-11-25 23:05:44233 def testKeyboardInterrupt(self):
234 os.kill(self._process.pid, signal.SIGINT)
235 self._process.wait(timeout=1)
236
Mohamed Heikalf746b57f2024-11-13 21:20:17237
238if __name__ == '__main__':
239 # Suppress logging messages.
240 unittest.main(buffer=True)