• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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