tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

fast_local_dev_server_test.py (9541B)


      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 pathlib
      9 import unittest
     10 import os
     11 import signal
     12 import socket
     13 import subprocess
     14 import sys
     15 import tempfile
     16 import time
     17 
     18 import fast_local_dev_server as server
     19 
     20 sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp'))
     21 from util import server_utils
     22 
     23 
     24 class 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 
     31 def 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 
     38 def pollServer():
     39  try:
     40    sendMessage({'message_type': server_utils.POLL_HEARTBEAT})
     41    return True
     42  except ConnectionRefusedError:
     43    return False
     44 
     45 
     46 def shouldSkip():
     47  if os.environ.get('ALLOW_EXTERNAL_BUILD_SERVER', None):
     48    return False
     49  return pollServer()
     50 
     51 
     52 def 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
     63 def 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 
     81 class 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 
    276 class 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 
    291 if __name__ == '__main__':
    292  # Suppress logging messages.
    293  unittest.main(buffer=True)