0
Files
src/build/android/fast_local_dev_server_test.py
Mohamed Heikal 3b8c955d48 Refactor build server to group build related work into a class
There is now a Build object/class that manages build specific work like
keeping track of build level statistics as well as creating and writing
to build logfiles.

Logfiles should no longer be leaked since we now can tell when a build
is "done" and close the logfile.

Title string is now based on global stats rather than per build to
better handle the case when two concurrent builds are updating the same
terminal title.

Added a global OptionsManager so as to not have to pipe the quiet option
everywhere.

Change-Id: I010982dc863ab45508957997c4ed8ad26902a4bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6249304
Commit-Queue: Mohamed Heikal <mheikal@chromium.org>
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1418922}
2025-02-11 14:33:40 -08:00

292 lines
9.2 KiB
Python
Executable File

#!/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.assertIn('[1/1]', 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('[1/2]', proc_result.stdout)
self.assertIn('--wait-for-idle', proc_result.stdout)
self.waitForTasksDone()
callServer(['--cancel-build', self._build_id])
self.waitForTasksDone()
proc_result = callServer(['--print-status', self._build_id])
self.assertIn('[2/2]', proc_result.stdout)
proc_result = callServer(['--print-status-all'])
self.assertIn('Siso finished', 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)