Files
scylladb/test/pylib/cpp/boost/prepare_instance.py
Andrei Chekun 8ef840a1c5 test.py: Add the possibility to run boost test from pytest
Add the possibility to run boost test from pytest.
Boost facade based on code from https://github.com/pytest-dev/pytest-cpp, but enhanced and rewritten to suite better.
2025-02-07 21:40:25 +01:00

163 lines
6.3 KiB
Python

#
# Copyright (C) 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#
from __future__ import annotations
import asyncio
import logging
import os
import shutil
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Generator
from test.pylib.host_registry import HostRegistry
from test.pylib.minio_server import MinioServer
from test.pylib.s3_proxy import S3ProxyServer
from test.pylib.s3_server_mock import MockS3Server
from test.pylib.util import LogPrefixAdapter
class PrepareChildProcessEnv:
"""
Class responsible to get environment variables from the main thread through the shared file and set them for the process
"""
def __init__(self, env_file: Path):
self.env_file = env_file
def prepare(self) -> None:
"""
Read the environment variables for S3 and MinIO from the file and set them for the process.
"""
timeout = 10
start_time = time.time()
sleep_for = 0.01
while True:
if os.path.exists(self.env_file):
with open(self.env_file, 'r') as file:
for line in file.readlines():
key, value = line.strip().split('=', 1)
os.environ[key] = value
break
if time.time() - start_time > timeout:
raise TimeoutError(f"Timeout waiting for file {self.env_file}")
# Sleep needed to wait when the controller will create a file with environment variables.
# Without sleep checking of the file existence will be too fast,
# so it will finish before the file is created
time.sleep(sleep_for)
sleep_for *=sleep_for
def cleanup(self) -> None:
"""
Fake method to have the same interfaces with Controller class.
"""
pass
def __enter__(self):
self.prepare()
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
class PrepareMainProcessEnv:
"""
A class responsible for starting additional services needed by tests.
It starts up a Minio server and an S3 mock server.
The environment settings are saved to a file for later consumption by child processes.
Class ensures that the necessary subdirectories exist or clean it if it exists
"""
def __init__(self, temp_dir: Path, modes: list[str], env_file: Path):
self.temp_dir = temp_dir
pytest_dirs = [self.temp_dir / mode / 'pytest' for mode in modes]
for directory in [self.temp_dir, *pytest_dirs]:
if not directory.exists():
os.makedirs(directory, exist_ok=True)
else:
shutil.rmtree(directory)
self.env_file = env_file
hosts = HostRegistry()
self.loop = asyncio.new_event_loop()
address_minio = self.loop.run_until_complete(hosts.lease_host())
address_s3_mock = self.loop.run_until_complete(hosts.lease_host())
self.address_s3_proxy = self.loop.run_until_complete(hosts.lease_host())
self.minio = MinioServer(self.temp_dir, address_minio, LogPrefixAdapter(logging.getLogger('minio'), {'prefix': 'minio'}))
self.mock_s3 = MockS3Server(address_s3_mock, 2012,
LogPrefixAdapter(logging.getLogger('s3_mock'), {'prefix': 's3_mock'}))
# S3 proxy initialized later because it needs to know Minis address and port that will be available only after
# Minio will start
self.proxy_s3 = None
def prepare(self) -> None:
"""
Start the S3 mock server and MinIO for the tests.
Create a file with environment variables for connecting to them.
"""
tasks = [
self.loop.create_task(self.minio.start()),
self.loop.create_task(self.mock_s3.start()),
]
self.loop.run_until_complete(asyncio.gather(*tasks))
envs = self.minio.get_envs_settings()
envs.update(self.mock_s3.get_envs_settings())
minio_uri = "http://" + envs[self.minio.ENV_ADDRESS] + ":" + envs[self.minio.ENV_PORT]
self.proxy_s3 = S3ProxyServer(self.address_s3_proxy, 9002, minio_uri, 3, int(time.time()),
LogPrefixAdapter(logging.getLogger('s3_proxy'), {'prefix': 's3_proxy'}))
self.loop.run_until_complete(self.proxy_s3.start())
envs.update(self.proxy_s3.get_envs_settings())
with open(self.env_file, 'w') as file:
for key, value in envs.items():
file.write(f"{key}={value}\n")
def cleanup(self) -> None:
"""
Stop the S3 mock server and MinIO
Remove the file with environment variables to not mess for consecutive runs.
"""
tasks = [
self.loop.create_task(self.minio.stop()),
self.loop.create_task(self.mock_s3.stop()),
self.loop.create_task(self.proxy_s3.stop()),
]
self.loop.run_until_complete(asyncio.gather(*tasks))
if os.path.exists(self.env_file):
self.env_file.unlink()
def __enter__(self):
try:
self.prepare()
except Exception:
self.cleanup()
raise
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
@contextmanager
def get_env_manager(temp_dir: Path, is_worker: bool, modes: list[str]) -> Generator[None, Any, None]:
"""
xdist helps to execute test in parallel.
For that purpose it creates one main controller and workers.
Pytest itself doesn't know if it's a worker or controller, so it will execute all fixtures and methods.
Tests need S3 mock server and minio to start only once for the whole run, since they can share the one instance and
share the environment variables with workers.
So the part of starting the servers executes on non-workers' processes.
That means when xdist isn't used, servers start as intended in the main process.
Tests on workers should know the endpoints of the servers, so the controller prepares this information.
According classes responsible for configuration controller and workers.
"""
env_file = Path(f"{temp_dir}/services_envs").absolute()
if is_worker:
with PrepareChildProcessEnv(env_file):
yield
else:
with PrepareMainProcessEnv(temp_dir, modes, env_file):
yield