Add JSON-RPC echo server for validation suite
Replaces the external jrpces binary dependency with a self-contained
Python script. The server supports TCP (newline-delimited JSON, port 4321)
and UDP (port 4323), handles JSON-RPC 1.0 and 2.0, and implements:
- echo(*args) -> [args, {}]
- unknown methods -> error {code: -32000, message: "function not found"}
test.tum is updated to launch jrpc_echo_server.py via python3 and wait
for the "ready" readiness message before running tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
134
test/validation/items/jsonrpc/jrpc_echo_server.py
Normal file
134
test/validation/items/jsonrpc/jrpc_echo_server.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""JSON-RPC echo server for the testium validation suite.
|
||||||
|
|
||||||
|
Listens on TCP (newline-delimited JSON) and UDP.
|
||||||
|
Supports JSON-RPC 1.0 and 2.0.
|
||||||
|
|
||||||
|
Handlers:
|
||||||
|
echo(*args) -> [args, {}]
|
||||||
|
<unknown> -> error {code: -32000, message: "function not found"}
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 jrpc_echo_server.py -c jrpces.ini
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch(method, params):
|
||||||
|
if method == "echo":
|
||||||
|
if not isinstance(params, list):
|
||||||
|
params = [params]
|
||||||
|
return True, [params, {}]
|
||||||
|
return False, {"code": -32000, "message": "function not found"}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(req, success, data):
|
||||||
|
req_id = req.get("id", None)
|
||||||
|
if req.get("jsonrpc") == "2.0":
|
||||||
|
if success:
|
||||||
|
return {"jsonrpc": "2.0", "result": data, "id": req_id}
|
||||||
|
else:
|
||||||
|
return {"jsonrpc": "2.0", "error": data, "id": req_id}
|
||||||
|
else:
|
||||||
|
if success:
|
||||||
|
return {"result": data, "error": None, "id": req_id}
|
||||||
|
else:
|
||||||
|
return {"result": None, "error": data, "id": req_id}
|
||||||
|
|
||||||
|
|
||||||
|
def handle(raw: str) -> str:
|
||||||
|
try:
|
||||||
|
req = json.loads(raw)
|
||||||
|
method = req.get("method", "")
|
||||||
|
params = req.get("params", [])
|
||||||
|
success, data = _dispatch(method, params)
|
||||||
|
return json.dumps(_build_response(req, success, data))
|
||||||
|
except Exception as exc:
|
||||||
|
return json.dumps({"result": None, "error": {"code": -32700, "message": str(exc)}, "id": None})
|
||||||
|
|
||||||
|
|
||||||
|
# ── TCP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _handle_tcp_client(conn):
|
||||||
|
buf = b""
|
||||||
|
with conn:
|
||||||
|
conn.settimeout(5.0)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
except (socket.timeout, ConnectionResetError, OSError):
|
||||||
|
break
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
buf += chunk
|
||||||
|
while b"\n" in buf:
|
||||||
|
line, buf = buf.split(b"\n", 1)
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
resp = handle(line.decode())
|
||||||
|
conn.sendall((resp + "\n").encode())
|
||||||
|
|
||||||
|
|
||||||
|
def _tcp_server(host, port):
|
||||||
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
srv.bind((host, port))
|
||||||
|
srv.listen(5)
|
||||||
|
srv.settimeout(1.0)
|
||||||
|
print(f"TCP listening on {host}:{port}", flush=True)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
conn, _ = srv.accept()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
threading.Thread(target=_handle_tcp_client, args=(conn,), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
# ── UDP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _udp_server(host, port):
|
||||||
|
srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
srv.bind((host, port))
|
||||||
|
print(f"UDP listening on {host}:{port}", flush=True)
|
||||||
|
while True:
|
||||||
|
data, addr = srv.recvfrom(65535)
|
||||||
|
resp = handle(data.decode())
|
||||||
|
srv.sendto(resp.encode(), addr)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="JSON-RPC echo server")
|
||||||
|
parser.add_argument("-c", "--config", required=True, help="Path to .ini config file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read(args.config)
|
||||||
|
|
||||||
|
tcp_host = cfg.get("jsonrpc_tcp", "host", fallback="0.0.0.0")
|
||||||
|
tcp_port = cfg.getint("jsonrpc_tcp", "port", fallback=4321)
|
||||||
|
udp_host = cfg.get("jsonrpc_udp", "host", fallback="0.0.0.0")
|
||||||
|
udp_port = cfg.getint("jsonrpc_udp", "port", fallback=4323)
|
||||||
|
|
||||||
|
tcp_thread = threading.Thread(target=_tcp_server, args=(tcp_host, tcp_port), daemon=True)
|
||||||
|
udp_thread = threading.Thread(target=_udp_server, args=(udp_host, udp_port), daemon=True)
|
||||||
|
tcp_thread.start()
|
||||||
|
udp_thread.start()
|
||||||
|
|
||||||
|
print("JSON-RPC echo server ready", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tcp_thread.join()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
|
|
||||||
- console:
|
- console:
|
||||||
name: json rpc echo server
|
name: json rpc echo server
|
||||||
doc: check if the jsonrpc echo server is installed
|
doc: check if jrpc_echo_server.py is available
|
||||||
console_name: jrpces
|
console_name: jrpces
|
||||||
key: $(test)_PASS
|
key: $(test)_PASS
|
||||||
steps:
|
steps:
|
||||||
- open:
|
- open:
|
||||||
protocol: terminal
|
protocol: terminal
|
||||||
- read_until: {expected: $(terminal_prompt), timeout: 1, no_fail: True}
|
- read_until: {expected: $(terminal_prompt), timeout: 1, no_fail: True}
|
||||||
- writeln: which jrpces
|
- writeln: test -f {{include_directory}}/jrpc_echo_server.py && echo JRPC_OK
|
||||||
- read_until: {expected: jrpces, timeout: 2}
|
- read_until: {expected: JRPC_OK, timeout: 2, no_fail: True}
|
||||||
|
|
||||||
- group:
|
- group:
|
||||||
name: jsonrpc tests
|
name: jsonrpc tests
|
||||||
condition: <| '/jrpces' in r'''$(cn_json rpc echo server)''' |>
|
condition: <| 'JRPC_OK' in r'''$(cn_json rpc echo server)''' |>
|
||||||
steps:
|
steps:
|
||||||
- console:
|
- console:
|
||||||
name: Start the json rpc echo server
|
name: Start the json rpc echo server
|
||||||
console_name: jrpces
|
console_name: jrpces
|
||||||
key: $(test)_PASS
|
key: $(test)_PASS
|
||||||
steps:
|
steps:
|
||||||
- writeln: jrpces -c {{include_directory}}/jrpces.ini
|
- writeln: python3 {{include_directory}}/jrpc_echo_server.py -c {{include_directory}}/jrpces.ini
|
||||||
- read_until: {expected: $(terminal_prompt), timeout: 1, no_fail: True}
|
- read_until: {expected: ready, timeout: 5}
|
||||||
|
|
||||||
- console:
|
- console:
|
||||||
name: Open the raw tcp Console
|
name: Open the raw tcp Console
|
||||||
|
|||||||
Reference in New Issue
Block a user