This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 00:00:06 +02:00
1 parent d23fa5e154
commit 81ab39e70a
51 files changed
+5352 -1489

No files matched your search

+7
View File
@@ -9,3 +9,10 @@ External development and QA helpers live under `tools/`; external platform integ
- `migration/` - application migration utilities, currently the Nginx Proxy Manager importer.
Keep these paths stable because Rust source, `build.rs` and shell entry points reference them directly.
## Stream TCP/UDP test helpers
- `start_stream_backends.sh` — launches two TCP echo mocks (9101, 9102) and two UDP echo/probe mocks (9201, 9202), defaulting to loopback, and terminates all on Ctrl+C.
- `test_stream_tcp.py`, `test_stream_udp.py` — independent `server`, `client` and `selftest` modes. See `docs/STREAMS.md` for RustProxy listener setup and test commands.
The selftests test only Python mock peers, **not** RustProxy forwarding.
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Start two TCP and two UDP mock backends for RustProxy Stream tests.
# Usage: ./start_stream_backends.sh [bind-host]
# Defaults to loopback for safety. Use 0.0.0.0 only on a controlled test network.
set -euo pipefail
DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
HOST="${1:-127.0.0.1}"
PYTHON="${PYTHON:-python3}"
pids=()
for file in test_stream_tcp.py test_stream_udp.py; do
if [[ ! -f "$DIR/$file" ]]; then
echo "Missing $DIR/$file (place the launcher next to the two test scripts)" >&2
exit 1
fi
done
cleanup() {
trap - EXIT INT TERM
if ((${#pids[@]})); then
kill "${pids[@]}" 2>/dev/null || true
wait "${pids[@]}" 2>/dev/null || true
fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
"$PYTHON" "$DIR/test_stream_tcp.py" server --host "$HOST" --port 9101 --backend-id tcp-a & pids+=("$!")
"$PYTHON" "$DIR/test_stream_tcp.py" server --host "$HOST" --port 9102 --backend-id tcp-b & pids+=("$!")
"$PYTHON" "$DIR/test_stream_udp.py" server --host "$HOST" --port 9201 --backend-id udp-a & pids+=("$!")
"$PYTHON" "$DIR/test_stream_udp.py" server --host "$HOST" --port 9202 --backend-id udp-b & pids+=("$!")
printf '\nMock backends for RustProxy (bind: %s):\n' "$HOST"
printf 'TCP: %s:9101 (tcp-a), %s:9102 (tcp-b)\n' "$HOST" "$HOST"
printf 'UDP: %s:9201 (udp-a), %s:9202 (udp-b)\n' "$HOST" "$HOST"
printf 'UDP health probe: PING => PONG\n'
printf 'Stop: Ctrl+C\n\n'
# Stop the group if any backend exits unexpectedly (e.g., port conflict).
wait -n "${pids[@]}"
echo 'A backend stopped; stopping all mock backends.' >&2
exit 1
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""TCP Stream black-box test: use against a running RustProxy and mock backends.
The 'selftest' mode validates this harness only; it does NOT run RustProxy.
All traffic uses loopback by default. Do not point it at a production service.
"""
from __future__ import annotations
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import secrets
import socket
import socketserver
import sys
import threading
import time
MAX_LINE = 1024
class TCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
request_queue_size = 128
class TCPBackend(socketserver.StreamRequestHandler):
def handle(self) -> None:
self.request.settimeout(10)
backend_id = self.server.backend_id.encode('ascii')
try:
while True:
line = self.rfile.readline(MAX_LINE + 1)
if not line or len(line) > MAX_LINE or not line.endswith(b'\n'):
return
self.wfile.write(backend_id + b'|' + line)
self.wfile.flush()
except (OSError, TimeoutError):
return
def run_server(host: str, port: int, backend_id: str) -> None:
with TCPServer((host, port), TCPBackend) as server:
server.backend_id = backend_id
print(f'TCP mock {backend_id} listening on {server.server_address}', flush=True)
try:
server.serve_forever(poll_interval=0.1)
except KeyboardInterrupt:
pass
def check_one(index: int, host: str, port: int, messages: int, timeout: float, hold_ms: float, nonce: str) -> str:
backend = None
with socket.create_connection((host, port), timeout=timeout) as connection:
connection.settimeout(timeout)
with connection.makefile('rb') as reader:
for seq in range(messages):
payload = f'rp-tcp:{nonce}:{index}:{seq}\n'.encode('ascii')
connection.sendall(payload)
response = reader.readline(MAX_LINE + 1)
try:
observed, echoed = response.split(b'|', 1)
except ValueError as exc:
raise AssertionError(f'session {index}: missing mock backend response: {response!r}') from exc
assert echoed == payload, f'session {index}: response mismatch: {response!r}'
assert observed, f'session {index}: empty backend ID'
if backend is None:
backend = observed
assert observed == backend, f'session {index}: TCP backend changed during connection'
if hold_ms and seq != messages - 1:
time.sleep(hold_ms / 1000)
return backend.decode('ascii')
def run_client(host: str, port: int, sessions: int, messages: int, parallel: int, timeout: float,
hold_ms: float, min_backends: int) -> Counter:
nonce = secrets.token_hex(8)
started = time.monotonic()
counts: Counter[str] = Counter()
with ThreadPoolExecutor(max_workers=parallel) as pool:
futures = [pool.submit(check_one, i, host, port, messages, timeout, hold_ms, nonce)
for i in range(sessions)]
for future in as_completed(futures):
counts[future.result()] += 1
assert len(counts) >= min_backends, f'observed {len(counts)} backend(s), required {min_backends}: {dict(counts)}'
print(f'TCP PASS: {sessions} sessions, {sessions * messages} messages, '
f'{len(counts)} backend(s), {time.monotonic() - started:.2f}s; assignments={dict(sorted(counts.items()))}')
return counts
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='mode', required=True)
server = sub.add_parser('server', help='run a local TCP mock backend')
server.add_argument('--host', default='127.0.0.1')
server.add_argument('--port', type=int, required=True)
server.add_argument('--backend-id', required=True, help='unique ASCII marker (e.g. node-a)')
client = sub.add_parser('client', help='test forwarding via an already-running RustProxy listener')
client.add_argument('--host', default='127.0.0.1', help='RustProxy TCP listener host')
client.add_argument('--port', type=int, required=True, help='RustProxy TCP listener port')
client.add_argument('--sessions', type=int, default=24)
client.add_argument('--messages', type=int, default=3)
client.add_argument('--parallel', type=int, default=8)
client.add_argument('--timeout', type=float, default=4.0)
client.add_argument('--hold-ms', type=float, default=20.0, help='pause between messages to keep TCP sessions alive')
client.add_argument('--min-backends', type=int, default=1, help='minimum distinct backend IDs observed')
sub.add_parser('selftest', help='test mock peer + harness directly, WITHOUT RustProxy')
args = parser.parse_args()
if args.mode == 'server':
if not args.backend_id.isascii() or not args.backend_id or '|' in args.backend_id or '\n' in args.backend_id:
parser.error('--backend-id must be nonempty ASCII without | or newline')
run_server(args.host, args.port, args.backend_id)
elif args.mode == 'client':
if min(args.port, args.sessions, args.messages, args.parallel, args.min_backends) < 1 or args.port > 65535 or args.timeout <= 0 or args.hold_ms < 0:
parser.error('invalid port, count, timeout or hold delay')
run_client(args.host, args.port, args.sessions, args.messages, args.parallel,
args.timeout, args.hold_ms, args.min_backends)
else:
with TCPServer(('127.0.0.1', 0), TCPBackend) as server:
server.backend_id = 'local-mock'
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
run_client('127.0.0.1', server.server_address[1], 8, 3, 4, 2.0, 5, 1)
print('SELFTEST PASS: tested mock TCP peer, not RustProxy')
finally:
server.shutdown()
thread.join(timeout=2)
if __name__ == '__main__':
try:
main()
except (OSError, AssertionError, UnicodeError) as exc:
print(f'TCP FAIL: {exc}', file=sys.stderr)
sys.exit(1)
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""UDP Stream black-box test: sends multiple packets from each stable client socket.
Start separately labeled mock backends and target an existing RustProxy UDP
listener. 'selftest' only validates this Python harness, NOT RustProxy.
By default, the server responds PONG to PING for an explicitly configured probe.
"""
from __future__ import annotations
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import secrets
import socket
import socketserver
import sys
import threading
import time
MAX_DATAGRAM = 1024
class UDPServer(socketserver.UDPServer):
allow_reuse_address = True
class UDPBackend(socketserver.BaseRequestHandler):
def handle(self) -> None:
data, connection = self.request
if data == b'PING':
reply = b'PONG'
elif data and len(data) <= MAX_DATAGRAM:
reply = self.server.backend_id.encode('ascii') + b'|' + data
else:
return
connection.sendto(reply, self.client_address)
def run_server(host: str, port: int, backend_id: str) -> None:
with UDPServer((host, port), UDPBackend) as server:
server.backend_id = backend_id
print(f'UDP mock {backend_id} listening on {server.server_address}; health probe PING -> PONG', flush=True)
try:
server.serve_forever(poll_interval=0.1)
except KeyboardInterrupt:
pass
def check_one(index: int, host: str, port: int, messages: int, timeout: float, pause_ms: float, nonce: str) -> str:
info = socket.getaddrinfo(host, port, type=socket.SOCK_DGRAM)[0]
family, sock_type, proto, _, address = info
backend = None
# One socket = one stable client identity for the entire UDP Stream session.
with socket.socket(family, sock_type, proto) as connection:
connection.settimeout(timeout)
for seq in range(messages):
payload = f'rp-udp:{nonce}:{index}:{seq}'.encode('ascii')
connection.sendto(payload, address)
response, _ = connection.recvfrom(MAX_DATAGRAM + 128)
try:
observed, echoed = response.split(b'|', 1)
except ValueError as exc:
raise AssertionError(f'session {index}: missing mock backend response: {response!r}') from exc
assert echoed == payload, f'session {index}: response mismatch {response!r}'
assert observed, f'session {index}: empty backend ID'
if backend is None:
backend = observed
assert observed == backend, f'session {index}: UDP backend changed for the same client socket'
if pause_ms and seq != messages - 1:
time.sleep(pause_ms / 1000)
return backend.decode('ascii')
def run_client(host: str, port: int, sessions: int, messages: int, parallel: int, timeout: float,
pause_ms: float, min_backends: int) -> Counter:
nonce = secrets.token_hex(8)
started = time.monotonic()
counts: Counter[str] = Counter()
with ThreadPoolExecutor(max_workers=parallel) as pool:
futures = [pool.submit(check_one, i, host, port, messages, timeout, pause_ms, nonce)
for i in range(sessions)]
for future in as_completed(futures):
counts[future.result()] += 1
assert len(counts) >= min_backends, f'observed {len(counts)} backend(s), required {min_backends}: {dict(counts)}'
print(f'UDP PASS: {sessions} client sockets, {sessions * messages} datagrams, '
f'{len(counts)} backend(s), {time.monotonic() - started:.2f}s; assignments={dict(sorted(counts.items()))}')
return counts
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='mode', required=True)
server = sub.add_parser('server', help='run local UDP echo and optional PING/PONG probe responder')
server.add_argument('--host', default='127.0.0.1')
server.add_argument('--port', type=int, required=True)
server.add_argument('--backend-id', required=True, help='unique ASCII marker (e.g. node-a)')
client = sub.add_parser('client', help='test forwarding via an already-running RustProxy UDP listener')
client.add_argument('--host', default='127.0.0.1', help='RustProxy UDP listener host')
client.add_argument('--port', type=int, required=True, help='RustProxy UDP listener port')
client.add_argument('--sessions', type=int, default=24)
client.add_argument('--messages', type=int, default=3)
client.add_argument('--parallel', type=int, default=8)
client.add_argument('--timeout', type=float, default=4.0)
client.add_argument('--pause-ms', type=float, default=20.0, help='pause between datagrams on each stable client socket')
client.add_argument('--min-backends', type=int, default=1, help='minimum distinct backend IDs observed')
sub.add_parser('selftest', help='test mock peer + harness directly, WITHOUT RustProxy')
args = parser.parse_args()
if args.mode == 'server':
if not args.backend_id.isascii() or not args.backend_id or '|' in args.backend_id or '\n' in args.backend_id:
parser.error('--backend-id must be nonempty ASCII without | or newline')
run_server(args.host, args.port, args.backend_id)
elif args.mode == 'client':
if min(args.port, args.sessions, args.messages, args.parallel, args.min_backends) < 1 or args.port > 65535 or args.timeout <= 0 or args.pause_ms < 0:
parser.error('invalid port, count, timeout or pause delay')
run_client(args.host, args.port, args.sessions, args.messages, args.parallel,
args.timeout, args.pause_ms, args.min_backends)
else:
with UDPServer(('127.0.0.1', 0), UDPBackend) as server:
server.backend_id = 'local-mock'
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
run_client('127.0.0.1', server.server_address[1], 8, 3, 4, 2.0, 5, 1)
print('SELFTEST PASS: tested mock UDP peer, not RustProxy')
finally:
server.shutdown()
thread.join(timeout=2)
if __name__ == '__main__':
try:
main()
except (OSError, AssertionError, UnicodeError) as exc:
print(f'UDP FAIL: {exc}', file=sys.stderr)
sys.exit(1)
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""TCP Stream black-box test: use against a running RustProxy and mock backends.
The 'selftest' mode validates this harness only; it does NOT run RustProxy.
All traffic uses loopback by default. Do not point it at a production service.
"""
from __future__ import annotations
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import secrets
import socket
import socketserver
import sys
import threading
import time
MAX_LINE = 1024
class TCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
request_queue_size = 128
class TCPBackend(socketserver.StreamRequestHandler):
def handle(self) -> None:
self.request.settimeout(10)
backend_id = self.server.backend_id.encode('ascii')
try:
while True:
line = self.rfile.readline(MAX_LINE + 1)
if not line or len(line) > MAX_LINE or not line.endswith(b'\n'):
return
self.wfile.write(backend_id + b'|' + line)
self.wfile.flush()
except (OSError, TimeoutError):
return
def run_server(host: str, port: int, backend_id: str) -> None:
with TCPServer((host, port), TCPBackend) as server:
server.backend_id = backend_id
print(f'TCP mock {backend_id} listening on {server.server_address}', flush=True)
try:
server.serve_forever(poll_interval=0.1)
except KeyboardInterrupt:
pass
def check_one(index: int, host: str, port: int, messages: int, timeout: float, hold_ms: float, nonce: str) -> str:
backend = None
with socket.create_connection((host, port), timeout=timeout) as connection:
connection.settimeout(timeout)
with connection.makefile('rb') as reader:
for seq in range(messages):
payload = f'rp-tcp:{nonce}:{index}:{seq}\n'.encode('ascii')
connection.sendall(payload)
response = reader.readline(MAX_LINE + 1)
try:
observed, echoed = response.split(b'|', 1)
except ValueError as exc:
raise AssertionError(f'session {index}: missing mock backend response: {response!r}') from exc
assert echoed == payload, f'session {index}: response mismatch: {response!r}'
assert observed, f'session {index}: empty backend ID'
if backend is None:
backend = observed
assert observed == backend, f'session {index}: TCP backend changed during connection'
if hold_ms and seq != messages - 1:
time.sleep(hold_ms / 1000)
return backend.decode('ascii')
def run_client(host: str, port: int, sessions: int, messages: int, parallel: int, timeout: float,
hold_ms: float, min_backends: int) -> Counter:
nonce = secrets.token_hex(8)
started = time.monotonic()
counts: Counter[str] = Counter()
with ThreadPoolExecutor(max_workers=parallel) as pool:
futures = [pool.submit(check_one, i, host, port, messages, timeout, hold_ms, nonce)
for i in range(sessions)]
for future in as_completed(futures):
counts[future.result()] += 1
assert len(counts) >= min_backends, f'observed {len(counts)} backend(s), required {min_backends}: {dict(counts)}'
print(f'TCP PASS: {sessions} sessions, {sessions * messages} messages, '
f'{len(counts)} backend(s), {time.monotonic() - started:.2f}s; assignments={dict(sorted(counts.items()))}')
return counts
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='mode', required=True)
server = sub.add_parser('server', help='run a local TCP mock backend')
server.add_argument('--host', default='127.0.0.1')
server.add_argument('--port', type=int, required=True)
server.add_argument('--backend-id', required=True, help='unique ASCII marker (e.g. node-a)')
client = sub.add_parser('client', help='test forwarding via an already-running RustProxy listener')
client.add_argument('--host', default='127.0.0.1', help='RustProxy TCP listener host')
client.add_argument('--port', type=int, required=True, help='RustProxy TCP listener port')
client.add_argument('--sessions', type=int, default=24)
client.add_argument('--messages', type=int, default=3)
client.add_argument('--parallel', type=int, default=8)
client.add_argument('--timeout', type=float, default=4.0)
client.add_argument('--hold-ms', type=float, default=20.0, help='pause between messages to keep TCP sessions alive')
client.add_argument('--min-backends', type=int, default=1, help='minimum distinct backend IDs observed')
sub.add_parser('selftest', help='test mock peer + harness directly, WITHOUT RustProxy')
args = parser.parse_args()
if args.mode == 'server':
if not args.backend_id.isascii() or not args.backend_id or '|' in args.backend_id or '\n' in args.backend_id:
parser.error('--backend-id must be nonempty ASCII without | or newline')
run_server(args.host, args.port, args.backend_id)
elif args.mode == 'client':
if min(args.port, args.sessions, args.messages, args.parallel, args.min_backends) < 1 or args.port > 65535 or args.timeout <= 0 or args.hold_ms < 0:
parser.error('invalid port, count, timeout or hold delay')
run_client(args.host, args.port, args.sessions, args.messages, args.parallel,
args.timeout, args.hold_ms, args.min_backends)
else:
with TCPServer(('127.0.0.1', 0), TCPBackend) as server:
server.backend_id = 'local-mock'
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
run_client('127.0.0.1', server.server_address[1], 8, 3, 4, 2.0, 5, 1)
print('SELFTEST PASS: tested mock TCP peer, not RustProxy')
finally:
server.shutdown()
thread.join(timeout=2)
if __name__ == '__main__':
try:
main()
except (OSError, AssertionError, UnicodeError) as exc:
print(f'TCP FAIL: {exc}', file=sys.stderr)
sys.exit(1)
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""UDP Stream black-box test: sends multiple packets from each stable client socket.
Start separately labeled mock backends and target an existing RustProxy UDP
listener. 'selftest' only validates this Python harness, NOT RustProxy.
By default, the server responds PONG to PING for an explicitly configured probe.
"""
from __future__ import annotations
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import secrets
import socket
import socketserver
import sys
import threading
import time
MAX_DATAGRAM = 1024
class UDPServer(socketserver.UDPServer):
allow_reuse_address = True
class UDPBackend(socketserver.BaseRequestHandler):
def handle(self) -> None:
data, connection = self.request
if data == b'PING':
reply = b'PONG'
elif data and len(data) <= MAX_DATAGRAM:
reply = self.server.backend_id.encode('ascii') + b'|' + data
else:
return
connection.sendto(reply, self.client_address)
def run_server(host: str, port: int, backend_id: str) -> None:
with UDPServer((host, port), UDPBackend) as server:
server.backend_id = backend_id
print(f'UDP mock {backend_id} listening on {server.server_address}; health probe PING -> PONG', flush=True)
try:
server.serve_forever(poll_interval=0.1)
except KeyboardInterrupt:
pass
def check_one(index: int, host: str, port: int, messages: int, timeout: float, pause_ms: float, nonce: str) -> str:
info = socket.getaddrinfo(host, port, type=socket.SOCK_DGRAM)[0]
family, sock_type, proto, _, address = info
backend = None
# One socket = one stable client identity for the entire UDP Stream session.
with socket.socket(family, sock_type, proto) as connection:
connection.settimeout(timeout)
for seq in range(messages):
payload = f'rp-udp:{nonce}:{index}:{seq}'.encode('ascii')
connection.sendto(payload, address)
response, _ = connection.recvfrom(MAX_DATAGRAM + 128)
try:
observed, echoed = response.split(b'|', 1)
except ValueError as exc:
raise AssertionError(f'session {index}: missing mock backend response: {response!r}') from exc
assert echoed == payload, f'session {index}: response mismatch {response!r}'
assert observed, f'session {index}: empty backend ID'
if backend is None:
backend = observed
assert observed == backend, f'session {index}: UDP backend changed for the same client socket'
if pause_ms and seq != messages - 1:
time.sleep(pause_ms / 1000)
return backend.decode('ascii')
def run_client(host: str, port: int, sessions: int, messages: int, parallel: int, timeout: float,
pause_ms: float, min_backends: int) -> Counter:
nonce = secrets.token_hex(8)
started = time.monotonic()
counts: Counter[str] = Counter()
with ThreadPoolExecutor(max_workers=parallel) as pool:
futures = [pool.submit(check_one, i, host, port, messages, timeout, pause_ms, nonce)
for i in range(sessions)]
for future in as_completed(futures):
counts[future.result()] += 1
assert len(counts) >= min_backends, f'observed {len(counts)} backend(s), required {min_backends}: {dict(counts)}'
print(f'UDP PASS: {sessions} client sockets, {sessions * messages} datagrams, '
f'{len(counts)} backend(s), {time.monotonic() - started:.2f}s; assignments={dict(sorted(counts.items()))}')
return counts
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='mode', required=True)
server = sub.add_parser('server', help='run local UDP echo and optional PING/PONG probe responder')
server.add_argument('--host', default='127.0.0.1')
server.add_argument('--port', type=int, required=True)
server.add_argument('--backend-id', required=True, help='unique ASCII marker (e.g. node-a)')
client = sub.add_parser('client', help='test forwarding via an already-running RustProxy UDP listener')
client.add_argument('--host', default='127.0.0.1', help='RustProxy UDP listener host')
client.add_argument('--port', type=int, required=True, help='RustProxy UDP listener port')
client.add_argument('--sessions', type=int, default=24)
client.add_argument('--messages', type=int, default=3)
client.add_argument('--parallel', type=int, default=8)
client.add_argument('--timeout', type=float, default=4.0)
client.add_argument('--pause-ms', type=float, default=20.0, help='pause between datagrams on each stable client socket')
client.add_argument('--min-backends', type=int, default=1, help='minimum distinct backend IDs observed')
sub.add_parser('selftest', help='test mock peer + harness directly, WITHOUT RustProxy')
args = parser.parse_args()
if args.mode == 'server':
if not args.backend_id.isascii() or not args.backend_id or '|' in args.backend_id or '\n' in args.backend_id:
parser.error('--backend-id must be nonempty ASCII without | or newline')
run_server(args.host, args.port, args.backend_id)
elif args.mode == 'client':
if min(args.port, args.sessions, args.messages, args.parallel, args.min_backends) < 1 or args.port > 65535 or args.timeout <= 0 or args.pause_ms < 0:
parser.error('invalid port, count, timeout or pause delay')
run_client(args.host, args.port, args.sessions, args.messages, args.parallel,
args.timeout, args.pause_ms, args.min_backends)
else:
with UDPServer(('127.0.0.1', 0), UDPBackend) as server:
server.backend_id = 'local-mock'
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
run_client('127.0.0.1', server.server_address[1], 8, 3, 4, 2.0, 5, 1)
print('SELFTEST PASS: tested mock UDP peer, not RustProxy')
finally:
server.shutdown()
thread.join(timeout=2)
if __name__ == '__main__':
try:
main()
except (OSError, AssertionError, UnicodeError) as exc:
print(f'UDP FAIL: {exc}', file=sys.stderr)
sys.exit(1)