mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-25 19:10:42 +00:00
With migration to pyest this fixture is useless. Removing and setting the session to the module for the most of the tests. Add dynamic_scope function to support running alternator fixtures in session scope, while Test and TestSuite are not deleted. This is for migration period, later on this function should be deleted.
239 lines
8.8 KiB
Python
239 lines
8.8 KiB
Python
#
|
|
# Copyright 2023-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
|
|
#
|
|
|
|
import os
|
|
import random
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import NamedTuple
|
|
|
|
import pytest
|
|
import requests.exceptions
|
|
|
|
from test import TOP_SRC_DIR, path_to
|
|
from test.nodetool.rest_api_mock import set_expected_requests, expected_request, get_expected_requests, \
|
|
get_unexpected_requests, expected_requests_manager
|
|
from test.pylib.db.model import Test
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addoption('--nodetool', action='store', choices=["scylla", "cassandra"], default="scylla",
|
|
help="Which nodetool implementation to run the tests against (default: %(default)s)")
|
|
parser.addoption('--nodetool-path', action='store', default=None,
|
|
help="Path to the nodetool binary,"
|
|
" with --nodetool=scylla, this should be the scylla binary,"
|
|
" with --nodetool=cassandra, this should be the nodetool binary")
|
|
parser.addoption('--jmx-path', action='store', default=None,
|
|
help="Path to the jmx binary, only used with --nodetool=cassandra")
|
|
parser.addoption('--run-within-unshare', action='store_true',
|
|
help="Setup the 'lo' network if launched with unshare(1)")
|
|
|
|
|
|
class ServerAddress(NamedTuple):
|
|
ip: str
|
|
port: int
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
async def server_address(request, testpy_test: None|Test):
|
|
# unshare(1) -rn drops us in a new network namespace in which the "lo" is
|
|
# not up yet, so let's set it up first.
|
|
if request.config.getoption('--run-within-unshare', default=False):
|
|
try:
|
|
args = "ip link set lo up".split()
|
|
subprocess.run(args, check=True)
|
|
except FileNotFoundError:
|
|
args = "/sbin/ifconfig lo up".split()
|
|
subprocess.run(args, check=True)
|
|
# we use a fixed ip and port, because the network namespace is not shared
|
|
ip = '127.0.0.1'
|
|
port = 12345
|
|
else:
|
|
if testpy_test is not None:
|
|
ip = await testpy_test.suite.hosts.lease_host()
|
|
else:
|
|
ip = f"127.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
|
|
# Ask the OS to pick a free port by binding to port 0. This avoids
|
|
# collisions with ports still in TIME_WAIT from a previous test module
|
|
# that used the same IP. SO_REUSEADDR is set on the probe socket so it
|
|
# can reclaim a TIME_WAIT port itself
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.bind((ip, 0))
|
|
port = s.getsockname()[1]
|
|
yield ServerAddress(ip, port)
|
|
if testpy_test is not None:
|
|
await testpy_test.suite.hosts.release_host(ip)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def rest_api_mock_server(request, server_address):
|
|
server_process = subprocess.Popen([sys.executable,
|
|
os.path.join(os.path.dirname(__file__), "rest_api_mock.py"),
|
|
server_address.ip,
|
|
str(server_address.port)])
|
|
# wait 5 seconds for the expected requests
|
|
timeout = 5
|
|
interval = 0.1
|
|
for _ in range(int(timeout / interval)):
|
|
returncode = server_process.poll()
|
|
if returncode is not None:
|
|
# process terminated
|
|
raise subprocess.CalledProcessError(returncode, server_process.args)
|
|
try:
|
|
get_expected_requests(server_address)
|
|
break
|
|
except requests.exceptions.ConnectionError:
|
|
time.sleep(interval)
|
|
except requests.exceptions.HTTPError as e:
|
|
if e.response.status_code == 404:
|
|
# The server is up but the endpoint is not ready yet, keep waiting
|
|
time.sleep(interval)
|
|
else:
|
|
raise
|
|
else:
|
|
server_process.terminate()
|
|
server_process.wait()
|
|
raise subprocess.TimeoutExpired(server_process.args, timeout)
|
|
|
|
try:
|
|
yield server_address
|
|
finally:
|
|
server_process.terminate()
|
|
server_process.wait()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def jmx(request, rest_api_mock_server):
|
|
if request.config.getoption("nodetool") == "scylla":
|
|
yield
|
|
return
|
|
|
|
jmx_path = request.config.getoption("jmx_path")
|
|
if jmx_path is None:
|
|
jmx_path = TOP_SRC_DIR / "tools" / "jmx" / "scripts" / "scylla-jmx"
|
|
else:
|
|
jmx_path = Path(jmx_path).absolute()
|
|
|
|
workdir = jmx_path.parent.parent
|
|
ip, api_port = rest_api_mock_server
|
|
expected_requests = [
|
|
expected_request(
|
|
"GET",
|
|
"/column_family/",
|
|
response=[{"ks": "system_schema",
|
|
"cf": "columns",
|
|
"type": "ColumnFamilies"},
|
|
{"ks": "system_schema",
|
|
"cf": "computed_columns",
|
|
"type": "ColumnFamilies"}]),
|
|
expected_request(
|
|
"GET",
|
|
"/stream_manager/",
|
|
response=[])]
|
|
set_expected_requests(rest_api_mock_server, expected_requests)
|
|
|
|
# Our nodetool launcher script ignores the host param, so this has to be 127.0.0.1, matching the internal default.
|
|
jmx_ip = "127.0.0.1"
|
|
jmx_port = random.randint(10000, 65535)
|
|
while jmx_port == api_port:
|
|
jmx_port = random.randint(10000, 65535)
|
|
|
|
jmx_process = subprocess.Popen(
|
|
[
|
|
jmx_path,
|
|
"-a", ip,
|
|
"-p", str(api_port),
|
|
"-ja", jmx_ip,
|
|
"-jp", str(jmx_port),
|
|
],
|
|
cwd=workdir, text=True)
|
|
|
|
# Wait until jmx starts up
|
|
# We rely on the expected requests being consumed for this
|
|
i = 0
|
|
while len(get_expected_requests(rest_api_mock_server)) > 0:
|
|
if i == 50: # 5 seconds
|
|
raise RuntimeError("timed out waiting for JMX to start")
|
|
time.sleep(0.1)
|
|
i += 1
|
|
|
|
yield jmx_ip, jmx_port
|
|
|
|
jmx_process.terminate()
|
|
jmx_process.wait()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def nodetool_path(request, build_mode):
|
|
if request.config.getoption("nodetool") == "scylla":
|
|
return path_to(build_mode, "scylla")
|
|
|
|
path = request.config.getoption("nodetool_path")
|
|
if path is not None:
|
|
return os.path.abspath(path)
|
|
|
|
return str(TOP_SRC_DIR / "java" / "bin" / "nodetool")
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def scylla_only(request):
|
|
if request.config.getoption("nodetool") != "scylla":
|
|
pytest.skip('Scylla-only test skipped')
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def cassandra_only(request):
|
|
if request.config.getoption("nodetool") != "cassandra":
|
|
pytest.skip('Cassandra-only test skipped')
|
|
|
|
def split_list(l, delim):
|
|
before = []
|
|
after = []
|
|
for elem in l:
|
|
(after if after or elem == delim else before).append(elem)
|
|
return (before, after)
|
|
|
|
@pytest.fixture(scope="module")
|
|
def nodetool(request, jmx, nodetool_path, rest_api_mock_server):
|
|
def invoker(method, *args, expected_requests=None, check_return_code=True):
|
|
with expected_requests_manager(rest_api_mock_server, expected_requests or []):
|
|
before, after = split_list(list(args), "--")
|
|
if request.config.getoption("nodetool") == "scylla":
|
|
api_ip, api_port = rest_api_mock_server
|
|
cmd = [nodetool_path, "nodetool", method] + before + ["--logger-log-level",
|
|
"scylla-nodetool=trace",
|
|
"-h", api_ip,
|
|
"-p", str(api_port)] + after
|
|
else:
|
|
jmx_ip, jmx_port = jmx
|
|
cmd = [nodetool_path, "-h", jmx_ip, "-p", str(jmx_port), method]
|
|
cmd += list(args)
|
|
suppressions_path = TOP_SRC_DIR / "ubsan-suppressions.supp"
|
|
env = {'UBSAN_OPTIONS': f'halt_on_error=1:abort_on_error=1:suppressions={suppressions_path}',
|
|
'ASAN_OPTIONS': f'disable_coredump=0:abort_on_error=1:detect_stack_use_after_return=1'}
|
|
res = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
|
sys.stdout.write(res.stdout)
|
|
sys.stderr.write(res.stderr)
|
|
|
|
expected_requests = [r for r in get_expected_requests(rest_api_mock_server)
|
|
if not r.exhausted()]
|
|
|
|
unexpected_requests = get_unexpected_requests(rest_api_mock_server)
|
|
|
|
# Check the return-code first, if the command failed probably not all requests were consumed
|
|
if check_return_code:
|
|
res.check_returncode()
|
|
assert len(expected_requests) == 0, ''.join(str(r) for r in expected_requests)
|
|
assert unexpected_requests == 0
|
|
|
|
return res
|
|
|
|
return invoker
|