127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
import shutil
|
|
import signal
|
|
import socket
|
|
import subprocess as sp
|
|
import time
|
|
|
|
# extracted from aioboto3
|
|
# https://github.com/terrycain/aioboto3/blob/16a1a1085191ebe6d40ee45d9588b2173738af0c/tests/mock_server.py
|
|
import pytest
|
|
import requests
|
|
|
|
from ray._common.network_utils import build_address
|
|
|
|
_proxy_bypass = {
|
|
"http": None,
|
|
"https": None,
|
|
}
|
|
|
|
|
|
def _is_port_available(host, port):
|
|
"""Check if a port is available for use."""
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind((host, port))
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _find_available_port(host, preferred_port, max_attempts=10):
|
|
"""Find an available port starting from preferred_port."""
|
|
|
|
# Try the preferred port first
|
|
if _is_port_available(host, preferred_port):
|
|
return preferred_port
|
|
|
|
# Try a wider range if preferred port is busy
|
|
for i in range(1, max_attempts):
|
|
port = preferred_port + i
|
|
if _is_port_available(host, port):
|
|
return port
|
|
|
|
# If all else fails, let the OS pick a port
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind((host, 0)) # Let OS pick port
|
|
_, port = s.getsockname()
|
|
return port
|
|
except OSError as e:
|
|
raise RuntimeError(
|
|
f"Could not find any available port starting from " f"{preferred_port}: {e}"
|
|
) from e
|
|
|
|
|
|
def start_service(service_name, host, port):
|
|
moto_svr_path = shutil.which("moto_server")
|
|
if not moto_svr_path:
|
|
pytest.skip("moto not installed")
|
|
|
|
# Always use port conflict resolution to be safe
|
|
port = _find_available_port(host, port)
|
|
|
|
# moto 5.x no longer accepts a service name argument - all services
|
|
# are served on a single endpoint
|
|
args = [moto_svr_path, "-H", host, "-p", str(port)]
|
|
# For debugging
|
|
# args = '{0} {1} -H {2} -p {3} 2>&1 | \
|
|
# tee -a /tmp/moto.log'.format(moto_svr_path, service_name, host, port)
|
|
process = sp.Popen(
|
|
args, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE
|
|
) # shell=True
|
|
url = f"http://{build_address(host, port)}"
|
|
|
|
for i in range(0, 30):
|
|
output = process.poll()
|
|
if output is not None:
|
|
print("moto_server exited status {0}".format(output))
|
|
stdout, stderr = process.communicate()
|
|
print("moto_server stdout: {0}".format(stdout))
|
|
print("moto_server stderr: {0}".format(stderr))
|
|
pytest.fail("Can not start service: {}".format(service_name))
|
|
|
|
try:
|
|
# we need to bypass the proxies due to monkeypatches
|
|
requests.get(url, timeout=5, proxies=_proxy_bypass)
|
|
break
|
|
except requests.exceptions.ConnectionError:
|
|
time.sleep(0.5)
|
|
else:
|
|
stop_process(process) # pytest.fail doesn't call stop_process
|
|
pytest.fail("Can not start service: {}".format(service_name))
|
|
|
|
return process, url
|
|
|
|
|
|
def stop_process(process):
|
|
"""Stop process with shorter timeout to prevent test hangs."""
|
|
if process is None or process.poll() is not None:
|
|
return # Already stopped
|
|
|
|
try:
|
|
process.send_signal(signal.SIGTERM)
|
|
process.communicate(timeout=20)
|
|
except sp.TimeoutExpired:
|
|
process.kill()
|
|
try:
|
|
process.communicate(timeout=5) # Short timeout for kill
|
|
except sp.TimeoutExpired:
|
|
print("Warning: Process cleanup timed out")
|
|
except Exception as e:
|
|
print(f"Warning: Error during process cleanup: {e}")
|
|
|
|
|
|
# TODO(Clark): We should be able to use "session" scope here, but we've found
|
|
# that the s3_fs fixture ends up hanging with S3 ops timing out (or the server
|
|
# being unreachable). This appears to only be an issue when using the tmp_dir
|
|
# fixture as the S3 dir path. We should fix this since "session" scope should
|
|
# reduce a lot of the per-test overhead (2x faster execution for IO methods in
|
|
# test_dataset.py).
|
|
@pytest.fixture(scope="function")
|
|
def s3_server():
|
|
host = "localhost"
|
|
port = 5002
|
|
process, url = start_service("s3", host, port)
|
|
yield url
|
|
stop_process(process)
|