157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import ssl
|
|
import sys
|
|
from contextlib import suppress
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from toki_socket.communicator import type_name_of
|
|
from toki_socket.packets.message_common_pb2 import TestData
|
|
from toki_socket.tcp_client import connect_tcp, connect_tcp_tls
|
|
from toki_socket.ws_client import connect_ws, connect_wss
|
|
|
|
HOST = "127.0.0.1"
|
|
WS_PATH = "/"
|
|
CONNECT_WINDOW = 3.0
|
|
REQUEST_WINDOW = 2.0
|
|
SERVER_READY_DELAY = 0.05
|
|
|
|
|
|
def parser_map():
|
|
return {type_name_of(TestData): TestData.FromString}
|
|
|
|
|
|
async def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
|
|
parser.add_argument("--port", type=int, required=True)
|
|
parser.add_argument("--phase", choices=["send-push", "requests"], default="send-push")
|
|
parser.add_argument("--cert", default=None)
|
|
args = parser.parse_args()
|
|
|
|
print(f"INFO typeName python={type_name_of(TestData)}")
|
|
|
|
try:
|
|
client = await dial_with_retry(args.mode, args.port, args.cert)
|
|
except Exception as exc:
|
|
fail("setup", str(exc))
|
|
raise SystemExit(1) from exc
|
|
await asyncio.sleep(SERVER_READY_DELAY)
|
|
|
|
try:
|
|
if args.phase == "send-push":
|
|
ok = await run_send_push(client)
|
|
else:
|
|
ok = await run_requests(client)
|
|
finally:
|
|
with suppress(Exception):
|
|
await asyncio.wait_for(client.close(), 1.0)
|
|
|
|
if not ok:
|
|
raise SystemExit(1)
|
|
|
|
|
|
async def dial_with_retry(mode: str, port: int, cert: str | None):
|
|
deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
|
|
last_error: Exception | None = None
|
|
while asyncio.get_running_loop().time() < deadline:
|
|
try:
|
|
return await asyncio.wait_for(dial(mode, port, cert), 0.3)
|
|
except Exception as exc:
|
|
last_error = exc
|
|
await asyncio.sleep(0.1)
|
|
raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")
|
|
|
|
|
|
async def dial(mode: str, port: int, cert: str | None):
|
|
if mode == "tcp":
|
|
return await connect_tcp(HOST, port, 0, 0, parser_map())
|
|
if mode == "ws":
|
|
return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
|
|
if mode == "tls":
|
|
if cert is None:
|
|
raise ValueError("--cert is required for tls mode")
|
|
ssl_ctx = ssl.create_default_context(cafile=cert)
|
|
ssl_ctx.check_hostname = False
|
|
return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
|
|
if mode == "wss":
|
|
if cert is None:
|
|
raise ValueError("--cert is required for wss mode")
|
|
ssl_ctx = ssl.create_default_context(cafile=cert)
|
|
ssl_ctx.check_hostname = False
|
|
return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
|
|
raise ValueError(f"unknown mode {mode!r}")
|
|
|
|
|
|
async def run_send_push(client) -> bool:
|
|
loop = asyncio.get_running_loop()
|
|
push = loop.create_future()
|
|
client.communicator.add_listener(
|
|
type_name_of(TestData),
|
|
lambda message: push.set_result(message) if not push.done() else None,
|
|
)
|
|
|
|
try:
|
|
await client.communicator.send(TestData(index=101, message="fire from python client"))
|
|
passed("1", "fire-and-forget sent")
|
|
|
|
message = await asyncio.wait_for(push, REQUEST_WINDOW)
|
|
if message.index != 200 or message.message != "push from go server":
|
|
fail("2", f"unexpected push index={message.index} message={message.message!r}")
|
|
return False
|
|
passed("2", "received push from go server")
|
|
return True
|
|
except Exception as exc:
|
|
fail("2", str(exc))
|
|
return False
|
|
|
|
|
|
async def run_requests(client) -> bool:
|
|
try:
|
|
single = await client.communicator.send_request(
|
|
TestData(index=21, message="single request from python"),
|
|
TestData,
|
|
timeout=REQUEST_WINDOW,
|
|
)
|
|
if single.index != 42 or single.message != "echo: single request from python":
|
|
fail("3", f"unexpected response index={single.index} message={single.message!r}")
|
|
return False
|
|
passed("3", "single request response matched")
|
|
except Exception as exc:
|
|
fail("3", str(exc))
|
|
return False
|
|
|
|
async def request_one(i: int) -> None:
|
|
index = 30 + i
|
|
message = f"multi request {i} from python"
|
|
res = await client.communicator.send_request(
|
|
TestData(index=index, message=message),
|
|
TestData,
|
|
timeout=REQUEST_WINDOW,
|
|
)
|
|
if res.index != index * 2 or res.message != "echo: " + message:
|
|
raise AssertionError(f"request {i} got index={res.index} message={res.message!r}")
|
|
|
|
try:
|
|
await asyncio.gather(*(request_one(i) for i in range(5)))
|
|
passed("4", "concurrent request responses matched")
|
|
return True
|
|
except Exception as exc:
|
|
fail("4", str(exc))
|
|
return False
|
|
|
|
|
|
def passed(scenario: str, detail: str) -> None:
|
|
print(f"PASS scenario={scenario} detail={detail}")
|
|
|
|
|
|
def fail(scenario: str, error: str) -> None:
|
|
print(f"FAIL scenario={scenario} error={error}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|