starpc 0.52.0 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +43 -0
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.py +306 -0
- package/echo/echo_srpc.pyi +85 -0
- package/go.mod +2 -2
- package/go.sum +14 -0
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +19 -5
- package/srpc/__init__.py +0 -0
- package/srpc/client.ts +20 -4
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server.test.ts +50 -0
- package/srpc/server.ts +22 -6
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# The checkout supplies generated message packages outside the installed wheel.
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from google.protobuf import empty_pb2
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
12
|
+
|
|
13
|
+
from echo import echo_pb2
|
|
14
|
+
from echo.echo_srpc import EchoerClient
|
|
15
|
+
from starpc.call import CallCancelledError, RemoteCallError
|
|
16
|
+
from starpc.client import Client
|
|
17
|
+
from starpc.stream import open_tcp_stream
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def main() -> None:
|
|
21
|
+
args = sys.argv[1:]
|
|
22
|
+
nested = args[:1] in (["--nested"], ["--nested-release"])
|
|
23
|
+
strict_nested = args[:1] == ["--nested-release"]
|
|
24
|
+
if nested:
|
|
25
|
+
args = args[1:]
|
|
26
|
+
if len(args) != 1 or ":" not in args[0]:
|
|
27
|
+
raise ValueError("usage: python-client.py [--nested] host:port")
|
|
28
|
+
host, port_text = args[0].rsplit(":", 1)
|
|
29
|
+
port = int(port_text)
|
|
30
|
+
raw_client = Client(lambda: open_tcp_stream(host, port))
|
|
31
|
+
client = EchoerClient(raw_client)
|
|
32
|
+
body = "hello world via python"
|
|
33
|
+
response = await client.echo(echo_pb2.EchoMsg(body=body))
|
|
34
|
+
if response.body != body:
|
|
35
|
+
raise RuntimeError(f"unexpected unary response: {response.body!r}")
|
|
36
|
+
empty_response = await client.echo(echo_pb2.EchoMsg())
|
|
37
|
+
if empty_response.body:
|
|
38
|
+
raise RuntimeError(f"unexpected empty unary response: {empty_response.body!r}")
|
|
39
|
+
received = [
|
|
40
|
+
message
|
|
41
|
+
async for message in client.echo_server_stream(echo_pb2.EchoMsg(body=body))
|
|
42
|
+
]
|
|
43
|
+
if len(received) != 5 or any(message.body != body for message in received):
|
|
44
|
+
raise RuntimeError("unexpected server-stream response")
|
|
45
|
+
client_stream = await client.echo_client_stream(_one_request(body))
|
|
46
|
+
if client_stream.body != body:
|
|
47
|
+
raise RuntimeError("unexpected client-stream response")
|
|
48
|
+
bidi = client.echo_bidi_stream(_one_request(body))
|
|
49
|
+
initial = await bidi.__anext__()
|
|
50
|
+
if initial.body != "hello from server":
|
|
51
|
+
raise RuntimeError("unexpected bidi initial response")
|
|
52
|
+
echoed = await bidi.__anext__()
|
|
53
|
+
if echoed.body != body:
|
|
54
|
+
raise RuntimeError("unexpected bidi echo")
|
|
55
|
+
try:
|
|
56
|
+
await bidi.__anext__()
|
|
57
|
+
except StopAsyncIteration:
|
|
58
|
+
pass
|
|
59
|
+
else:
|
|
60
|
+
raise RuntimeError("bidi stream did not terminate")
|
|
61
|
+
empty = await client.do_nothing(empty_pb2.Empty())
|
|
62
|
+
if empty.ByteSize() != 0:
|
|
63
|
+
raise RuntimeError("unexpected DoNothing response")
|
|
64
|
+
|
|
65
|
+
unknown = await raw_client.open_call("missing.Service", "Missing")
|
|
66
|
+
try:
|
|
67
|
+
await unknown.finish()
|
|
68
|
+
try:
|
|
69
|
+
await unknown.receive()
|
|
70
|
+
except RemoteCallError:
|
|
71
|
+
pass
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError("unknown method did not return a remote error")
|
|
74
|
+
finally:
|
|
75
|
+
await unknown.aclose()
|
|
76
|
+
|
|
77
|
+
cancelled = await raw_client.open_call("echo.Echoer", "EchoBidiStream")
|
|
78
|
+
await cancelled.cancel()
|
|
79
|
+
try:
|
|
80
|
+
await cancelled.wait_closed()
|
|
81
|
+
except CallCancelledError:
|
|
82
|
+
pass
|
|
83
|
+
await cancelled.aclose()
|
|
84
|
+
if nested:
|
|
85
|
+
await run_nested_test(client, strict_nested)
|
|
86
|
+
print("All tests passed.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def run_nested_test(client: EchoerClient, strict: bool = False) -> None:
|
|
90
|
+
from starpc.rpcstream import (
|
|
91
|
+
RpcStreamRemoteError,
|
|
92
|
+
build_rpc_stream_open_stream,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
nested_client = Client(build_rpc_stream_open_stream("test", client.rpc_stream))
|
|
96
|
+
nested_echo = EchoerClient(nested_client)
|
|
97
|
+
body = "hello world via nested python"
|
|
98
|
+
response = await nested_echo.echo(echo_pb2.EchoMsg(body=body))
|
|
99
|
+
if response.body != body:
|
|
100
|
+
raise RuntimeError(f"unexpected nested unary response: {response.body!r}")
|
|
101
|
+
|
|
102
|
+
if strict:
|
|
103
|
+
try:
|
|
104
|
+
await nested_echo.echo(echo_pb2.EchoMsg(body="__nested_error__"))
|
|
105
|
+
except RemoteCallError:
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError("terminal nested error unexpectedly succeeded")
|
|
109
|
+
|
|
110
|
+
if strict:
|
|
111
|
+
missing = Client(build_rpc_stream_open_stream("missing", client.rpc_stream))
|
|
112
|
+
try:
|
|
113
|
+
await missing.open_call("echo.Echoer", "Echo")
|
|
114
|
+
except RpcStreamRemoteError:
|
|
115
|
+
pass
|
|
116
|
+
else:
|
|
117
|
+
raise RuntimeError("unknown component unexpectedly succeeded")
|
|
118
|
+
|
|
119
|
+
terminal = await nested_client.open_call("missing.Service", "Missing")
|
|
120
|
+
try:
|
|
121
|
+
await terminal.receive()
|
|
122
|
+
except RemoteCallError:
|
|
123
|
+
pass
|
|
124
|
+
else:
|
|
125
|
+
raise RuntimeError("unknown nested method unexpectedly succeeded")
|
|
126
|
+
finally:
|
|
127
|
+
await terminal.aclose()
|
|
128
|
+
|
|
129
|
+
cancelled = await nested_client.open_call("echo.Echoer", "EchoBidiStream")
|
|
130
|
+
await cancelled.send(
|
|
131
|
+
echo_pb2.EchoMsg(body="nested later data").SerializeToString(deterministic=True)
|
|
132
|
+
)
|
|
133
|
+
await cancelled.cancel()
|
|
134
|
+
try:
|
|
135
|
+
await cancelled.wait_closed()
|
|
136
|
+
except CallCancelledError:
|
|
137
|
+
pass
|
|
138
|
+
await cancelled.aclose()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _one_request(body: str) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
142
|
+
yield echo_pb2.EchoMsg(body=body)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# The checkout supplies generated message packages outside the installed wheel.
|
|
4
|
+
import asyncio
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from google.protobuf import empty_pb2
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
13
|
+
|
|
14
|
+
from echo import echo_pb2
|
|
15
|
+
from echo.echo_srpc import register_echoer
|
|
16
|
+
from rpcstream import rpcstream_pb2
|
|
17
|
+
from starpc.rpcstream import ComponentRegistry, handle_rpc_stream
|
|
18
|
+
from starpc.server import Server, ServiceRegistry
|
|
19
|
+
from starpc.stream import TCPStreamServer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Implementation:
|
|
23
|
+
def __init__(self, components: ComponentRegistry) -> None:
|
|
24
|
+
self._components = components
|
|
25
|
+
self._release_task: asyncio.Task[None] | None = None
|
|
26
|
+
self._release_complete = asyncio.Event()
|
|
27
|
+
|
|
28
|
+
async def _release_component(self) -> None:
|
|
29
|
+
try:
|
|
30
|
+
await self._components.unregister("release")
|
|
31
|
+
finally:
|
|
32
|
+
self._release_complete.set()
|
|
33
|
+
|
|
34
|
+
async def wait_for_release(self) -> None:
|
|
35
|
+
task = self._release_task
|
|
36
|
+
if task is not None:
|
|
37
|
+
await task
|
|
38
|
+
|
|
39
|
+
async def echo(self, request: echo_pb2.EchoMsg) -> echo_pb2.EchoMsg:
|
|
40
|
+
if request.body == "__nested_error__":
|
|
41
|
+
raise RuntimeError("nested terminal error")
|
|
42
|
+
if request.body == "__nested_release__":
|
|
43
|
+
if self._release_task is None:
|
|
44
|
+
self._release_task = asyncio.create_task(self._release_component())
|
|
45
|
+
await asyncio.Future()
|
|
46
|
+
if request.body == "__nested_release_status__":
|
|
47
|
+
await self._release_complete.wait()
|
|
48
|
+
await self.wait_for_release()
|
|
49
|
+
return echo_pb2.EchoMsg(body="released")
|
|
50
|
+
return echo_pb2.EchoMsg(body=request.body)
|
|
51
|
+
|
|
52
|
+
async def echo_server_stream(
|
|
53
|
+
self, request: echo_pb2.EchoMsg
|
|
54
|
+
) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
55
|
+
for _ in range(5):
|
|
56
|
+
yield echo_pb2.EchoMsg(body=request.body)
|
|
57
|
+
await asyncio.sleep(0.2)
|
|
58
|
+
|
|
59
|
+
async def echo_client_stream(
|
|
60
|
+
self, requests: AsyncIterator[echo_pb2.EchoMsg]
|
|
61
|
+
) -> echo_pb2.EchoMsg:
|
|
62
|
+
async for request in requests:
|
|
63
|
+
return request
|
|
64
|
+
return echo_pb2.EchoMsg()
|
|
65
|
+
|
|
66
|
+
async def echo_bidi_stream(
|
|
67
|
+
self, requests: AsyncIterator[echo_pb2.EchoMsg]
|
|
68
|
+
) -> AsyncIterator[echo_pb2.EchoMsg]:
|
|
69
|
+
yield echo_pb2.EchoMsg(body="hello from server")
|
|
70
|
+
async for request in requests:
|
|
71
|
+
yield request
|
|
72
|
+
|
|
73
|
+
def rpc_stream(
|
|
74
|
+
self, requests: AsyncIterator[rpcstream_pb2.RpcStreamPacket]
|
|
75
|
+
) -> AsyncIterator[rpcstream_pb2.RpcStreamPacket]:
|
|
76
|
+
return handle_rpc_stream(requests, self._components)
|
|
77
|
+
|
|
78
|
+
async def do_nothing(self, request: empty_pb2.Empty) -> empty_pb2.Empty:
|
|
79
|
+
return request
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def main() -> None:
|
|
83
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
|
84
|
+
registry = ServiceRegistry()
|
|
85
|
+
components = ComponentRegistry()
|
|
86
|
+
await components.register("test", Server(registry))
|
|
87
|
+
await components.register("release", Server(registry))
|
|
88
|
+
implementation = Implementation(components)
|
|
89
|
+
register_echoer(registry, implementation)
|
|
90
|
+
server = TCPStreamServer()
|
|
91
|
+
await server.start("127.0.0.1", port)
|
|
92
|
+
host, bound_port = server.address
|
|
93
|
+
print(f"LISTENING {host}:{bound_port}", flush=True)
|
|
94
|
+
rpc_server = Server(registry)
|
|
95
|
+
tasks: set[asyncio.Task[None]] = set()
|
|
96
|
+
errors: list[BaseException] = []
|
|
97
|
+
stopping = asyncio.Event()
|
|
98
|
+
loop = asyncio.get_running_loop()
|
|
99
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
100
|
+
loop.add_signal_handler(sig, stopping.set)
|
|
101
|
+
|
|
102
|
+
def observe_task(task: asyncio.Task[None]) -> None:
|
|
103
|
+
tasks.discard(task)
|
|
104
|
+
if not task.cancelled() and (error := task.exception()) is not None:
|
|
105
|
+
errors.append(error)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
while not stopping.is_set():
|
|
109
|
+
accepting = asyncio.create_task(server.accept())
|
|
110
|
+
stopped = asyncio.create_task(stopping.wait())
|
|
111
|
+
done, _ = await asyncio.wait(
|
|
112
|
+
{accepting, stopped}, return_when=asyncio.FIRST_COMPLETED
|
|
113
|
+
)
|
|
114
|
+
if stopped in done:
|
|
115
|
+
accepting.cancel()
|
|
116
|
+
await asyncio.gather(accepting, return_exceptions=True)
|
|
117
|
+
break
|
|
118
|
+
stopped.cancel()
|
|
119
|
+
await asyncio.gather(stopped, return_exceptions=True)
|
|
120
|
+
stream = accepting.result()
|
|
121
|
+
task = asyncio.create_task(rpc_server.serve(stream))
|
|
122
|
+
tasks.add(task)
|
|
123
|
+
task.add_done_callback(observe_task)
|
|
124
|
+
finally:
|
|
125
|
+
await server.aclose()
|
|
126
|
+
for task in tasks:
|
|
127
|
+
task.cancel()
|
|
128
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
129
|
+
await implementation.wait_for_release()
|
|
130
|
+
await components.unregister("test")
|
|
131
|
+
await components.unregister("release")
|
|
132
|
+
if errors:
|
|
133
|
+
raise ExceptionGroup("cross-language server tasks failed", errors)
|
|
134
|
+
if components._components or components._retired:
|
|
135
|
+
raise RuntimeError("nested component cleanup is incomplete")
|
|
136
|
+
print("NESTED_CLEAN", flush=True)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
asyncio.run(main())
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Cross-language integration tests for starpc.
|
|
3
|
-
# Runs all
|
|
3
|
+
# Runs all 21 server/client combinations across Go, TypeScript, Rust, C++, and Python.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# ./run.bash
|
|
7
|
-
# ./run.bash go:ts
|
|
8
|
-
# ./run.bash
|
|
6
|
+
# ./run.bash # Run all pairs
|
|
7
|
+
# ./run.bash go:ts # Run go-server+ts-client and ts-server+go-client
|
|
8
|
+
# ./run.bash python:go # Run python-server+go-client and go-server+python-client
|
|
9
|
+
# ./run.bash --nested ts:python # Run nested TypeScript/Python pairs
|
|
10
|
+
# ./run.bash --nested go:ts go:python ts:python # Run nested Go/TypeScript/Python pairs
|
|
9
11
|
set -eo pipefail
|
|
10
12
|
|
|
11
13
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -14,25 +16,79 @@ REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
14
16
|
# Fixes errors with the generated esm using require()
|
|
15
17
|
ESM_BANNER='import{fileURLToPath}from"node:url";import{dirname}from"node:path";import{createRequire as topLevelCreateRequire}from"node:module";const require=topLevelCreateRequire(import.meta.url);const __filename=fileURLToPath(import.meta.url);const __dirname=dirname(__filename);'
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
NESTED=false
|
|
20
|
+
FILTERS=()
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
if [ "$arg" = "--nested" ]; then
|
|
23
|
+
NESTED=true
|
|
24
|
+
else
|
|
25
|
+
FILTERS+=("$arg")
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
needs_language() {
|
|
30
|
+
local language="$1"
|
|
31
|
+
if [ ${#FILTERS[@]} -eq 0 ]; then
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
for filter in "${FILTERS[@]}"; do
|
|
35
|
+
if [[ ":${filter}:" == *":${language}:"* ]]; then
|
|
36
|
+
return 0
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
PASSED=0
|
|
20
43
|
FAILED=0
|
|
21
44
|
ERRORS=""
|
|
22
45
|
|
|
46
|
+
SERVER_PID=""
|
|
23
47
|
SERVER_LOG=""
|
|
48
|
+
SERVER_EXIT_STATUS=0
|
|
49
|
+
CLIENT_PID=""
|
|
50
|
+
CLIENT_OUT=""
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
cleanup() {
|
|
53
|
+
if [ -n "${CLIENT_PID:-}" ]; then
|
|
54
|
+
kill "$CLIENT_PID" 2>/dev/null || true
|
|
55
|
+
wait "$CLIENT_PID" 2>/dev/null || true
|
|
56
|
+
CLIENT_PID=""
|
|
57
|
+
fi
|
|
58
|
+
if [ -n "${SERVER_PID:-}" ]; then
|
|
59
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
60
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
61
|
+
SERVER_PID=""
|
|
62
|
+
fi
|
|
63
|
+
if [ -n "${SERVER_LOG:-}" ]; then
|
|
64
|
+
rm -f "$SERVER_LOG"
|
|
65
|
+
SERVER_LOG=""
|
|
66
|
+
fi
|
|
67
|
+
if [ -n "${CLIENT_OUT:-}" ]; then
|
|
68
|
+
rm -f "$CLIENT_OUT"
|
|
69
|
+
CLIENT_OUT=""
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
trap cleanup EXIT
|
|
74
|
+
trap 'exit 130' INT
|
|
75
|
+
trap 'exit 143' TERM
|
|
76
|
+
|
|
77
|
+
# should_run checks if a server/client pair matches the active filters.
|
|
26
78
|
# Returns 0 (true) if the test should run, 1 (false) otherwise.
|
|
27
79
|
should_run() {
|
|
28
80
|
local test_name="$1"
|
|
29
81
|
if [ ${#FILTERS[@]} -eq 0 ]; then
|
|
30
82
|
return 0
|
|
31
83
|
fi
|
|
84
|
+
local server_language="${test_name%%-server*}"
|
|
85
|
+
local client_language="${test_name#*+ }"
|
|
86
|
+
client_language="${client_language%%-client*}"
|
|
32
87
|
for filter in "${FILTERS[@]}"; do
|
|
33
88
|
local lang1="${filter%%:*}"
|
|
34
89
|
local lang2="${filter##*:}"
|
|
35
|
-
if [
|
|
90
|
+
if { [ "$server_language" = "$lang1" ] && [ "$client_language" = "$lang2" ]; } ||
|
|
91
|
+
{ [ "$server_language" = "$lang2" ] && [ "$client_language" = "$lang1" ]; }; then
|
|
36
92
|
return 0
|
|
37
93
|
fi
|
|
38
94
|
done
|
|
@@ -42,32 +98,39 @@ should_run() {
|
|
|
42
98
|
# Build all binaries.
|
|
43
99
|
echo "=== Building all integration binaries ==="
|
|
44
100
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
go build -o "$SCRIPT_DIR/go-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
echo
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
101
|
+
if needs_language go; then
|
|
102
|
+
echo "Building Go server/client..."
|
|
103
|
+
go build -o "$SCRIPT_DIR/go-server/go-server" "$SCRIPT_DIR/go-server/"
|
|
104
|
+
go build -o "$SCRIPT_DIR/go-client/go-client" "$SCRIPT_DIR/go-client/"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if needs_language ts; then
|
|
108
|
+
echo "Building TypeScript server/client..."
|
|
109
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-server.ts" \
|
|
110
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
111
|
+
--banner:js="$ESM_BANNER" \
|
|
112
|
+
--outfile="$SCRIPT_DIR/ts-server.mjs"
|
|
113
|
+
"$REPO_DIR/node_modules/.bin/esbuild" "$SCRIPT_DIR/ts-client.ts" \
|
|
114
|
+
--bundle --sourcemap --platform=node --format=esm \
|
|
115
|
+
--banner:js="$ESM_BANNER" \
|
|
116
|
+
--outfile="$SCRIPT_DIR/ts-client.mjs"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if needs_language rust; then
|
|
120
|
+
echo "Building Rust server/client..."
|
|
121
|
+
cargo build --release -p echo-example --bin integration-server --bin integration-client
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if needs_language cpp; then
|
|
125
|
+
echo "Vendoring Go dependencies (needed for C++ build)..."
|
|
126
|
+
go mod vendor
|
|
127
|
+
echo "Building C++ server/client..."
|
|
128
|
+
mkdir -p "$REPO_DIR/build"
|
|
129
|
+
pushd "$REPO_DIR/build" > /dev/null
|
|
130
|
+
cmake "$REPO_DIR" -DCMAKE_BUILD_TYPE=Release > /dev/null 2>&1
|
|
131
|
+
cmake --build . --target cpp-integration-server cpp-integration-client --parallel > /dev/null 2>&1
|
|
132
|
+
popd > /dev/null
|
|
133
|
+
fi
|
|
71
134
|
|
|
72
135
|
# Binary paths.
|
|
73
136
|
GO_SERVER="$SCRIPT_DIR/go-server/go-server"
|
|
@@ -78,6 +141,7 @@ RUST_SERVER="$REPO_DIR/target/release/integration-server"
|
|
|
78
141
|
RUST_CLIENT="$REPO_DIR/target/release/integration-client"
|
|
79
142
|
CPP_SERVER="$REPO_DIR/build/cpp-integration-server"
|
|
80
143
|
CPP_CLIENT="$REPO_DIR/build/cpp-integration-client"
|
|
144
|
+
PYTHON=(uv run --project "$REPO_DIR" python)
|
|
81
145
|
|
|
82
146
|
start_server() {
|
|
83
147
|
SERVER_LOG=$(mktemp)
|
|
@@ -95,8 +159,9 @@ start_server() {
|
|
|
95
159
|
SERVER_ADDR=$(grep 'LISTENING' "$SERVER_LOG" 2>/dev/null | awk '{print $2}')
|
|
96
160
|
if [ -z "$SERVER_ADDR" ]; then
|
|
97
161
|
echo "FAILED: server did not start"
|
|
98
|
-
kill $SERVER_PID 2>/dev/null || true
|
|
99
|
-
wait $SERVER_PID 2>/dev/null || true
|
|
162
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
163
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
164
|
+
SERVER_PID=""
|
|
100
165
|
rm -f "$SERVER_LOG"
|
|
101
166
|
SERVER_LOG=""
|
|
102
167
|
return 1
|
|
@@ -105,8 +170,36 @@ start_server() {
|
|
|
105
170
|
}
|
|
106
171
|
|
|
107
172
|
stop_server() {
|
|
108
|
-
kill $SERVER_PID 2>/dev/null || true
|
|
109
|
-
wait $SERVER_PID 2>/dev/null
|
|
173
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
174
|
+
if wait "$SERVER_PID" 2>/dev/null; then
|
|
175
|
+
SERVER_EXIT_STATUS=0
|
|
176
|
+
else
|
|
177
|
+
SERVER_EXIT_STATUS=$?
|
|
178
|
+
fi
|
|
179
|
+
SERVER_PID=""
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
python_server_passed() {
|
|
183
|
+
local status="$1"
|
|
184
|
+
local log="$2"
|
|
185
|
+
[ "$status" -eq 0 ] && grep -qx "NESTED_CLEAN" "$log"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
verify_python_server_guard() {
|
|
189
|
+
local log
|
|
190
|
+
log="$(mktemp)"
|
|
191
|
+
printf '%s\n' "NESTED_CLEAN" >"$log"
|
|
192
|
+
if python_server_passed 1 "$log"; then
|
|
193
|
+
rm -f "$log"
|
|
194
|
+
echo "runner guard accepted a nonzero Python server" >&2
|
|
195
|
+
return 1
|
|
196
|
+
fi
|
|
197
|
+
if ! python_server_passed 0 "$log"; then
|
|
198
|
+
rm -f "$log"
|
|
199
|
+
echo "runner guard rejected a clean Python server" >&2
|
|
200
|
+
return 1
|
|
201
|
+
fi
|
|
202
|
+
rm -f "$log"
|
|
110
203
|
}
|
|
111
204
|
|
|
112
205
|
# run_pair <test_name> <server_args...> -- <client_args...>
|
|
@@ -142,13 +235,25 @@ run_pair() {
|
|
|
142
235
|
return
|
|
143
236
|
fi
|
|
144
237
|
|
|
145
|
-
|
|
146
|
-
client_out=$(mktemp)
|
|
238
|
+
CLIENT_OUT=$(mktemp)
|
|
147
239
|
local client_ok=false
|
|
148
|
-
|
|
240
|
+
timeout 60 "${cli_args[@]}" "$SERVER_ADDR" > "$CLIENT_OUT" 2>&1 &
|
|
241
|
+
CLIENT_PID=$!
|
|
242
|
+
if wait "$CLIENT_PID"; then
|
|
149
243
|
client_ok=true
|
|
150
244
|
fi
|
|
245
|
+
CLIENT_PID=""
|
|
151
246
|
stop_server
|
|
247
|
+
if $NESTED && [[ "$test_name" == python-server* ]]; then
|
|
248
|
+
if ! verify_python_server_guard; then
|
|
249
|
+
client_ok=false
|
|
250
|
+
echo " Python server result guard regression failed"
|
|
251
|
+
elif ! python_server_passed "$SERVER_EXIT_STATUS" "$SERVER_LOG"; then
|
|
252
|
+
client_ok=false
|
|
253
|
+
echo " Python server failed nested shutdown (status $SERVER_EXIT_STATUS):"
|
|
254
|
+
sed 's/^/ /' "$SERVER_LOG"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
152
257
|
|
|
153
258
|
if $client_ok; then
|
|
154
259
|
echo "PASSED"
|
|
@@ -158,9 +263,10 @@ run_pair() {
|
|
|
158
263
|
FAILED=$((FAILED + 1))
|
|
159
264
|
ERRORS="${ERRORS}\n ${test_name}"
|
|
160
265
|
echo " client output:"
|
|
161
|
-
sed 's/^/ /' "$
|
|
266
|
+
sed 's/^/ /' "$CLIENT_OUT"
|
|
162
267
|
fi
|
|
163
|
-
rm -f "$
|
|
268
|
+
rm -f "$CLIENT_OUT" "$SERVER_LOG"
|
|
269
|
+
CLIENT_OUT=""
|
|
164
270
|
SERVER_LOG=""
|
|
165
271
|
}
|
|
166
272
|
|
|
@@ -168,29 +274,48 @@ echo ""
|
|
|
168
274
|
echo "=== Running cross-language integration tests ==="
|
|
169
275
|
echo ""
|
|
170
276
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
run_pair "go-server +
|
|
174
|
-
run_pair "go-server +
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
run_pair "
|
|
180
|
-
run_pair "
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
run_pair "
|
|
185
|
-
run_pair "
|
|
186
|
-
run_pair "
|
|
187
|
-
run_pair "
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
run_pair "
|
|
191
|
-
run_pair "
|
|
192
|
-
run_pair "
|
|
193
|
-
run_pair "
|
|
277
|
+
if $NESTED; then
|
|
278
|
+
# Nested streams have maintained Go, TypeScript, and Python endpoints.
|
|
279
|
+
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT" --nested
|
|
280
|
+
run_pair "go-server + python-client" "$GO_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py" --nested
|
|
281
|
+
|
|
282
|
+
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT" --nested
|
|
283
|
+
run_pair "ts-server + python-client" node "$TS_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py" --nested
|
|
284
|
+
|
|
285
|
+
run_pair "python-server + go-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "$GO_CLIENT" --nested-release
|
|
286
|
+
run_pair "python-server + ts-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- node "$TS_CLIENT" --nested-release
|
|
287
|
+
else
|
|
288
|
+
# Go server combinations
|
|
289
|
+
run_pair "go-server + go-client" "$GO_SERVER" -- "$GO_CLIENT"
|
|
290
|
+
run_pair "go-server + rust-client" "$GO_SERVER" -- "$RUST_CLIENT"
|
|
291
|
+
run_pair "go-server + ts-client" "$GO_SERVER" -- node "$TS_CLIENT"
|
|
292
|
+
run_pair "go-server + cpp-client" "$GO_SERVER" -- "$CPP_CLIENT"
|
|
293
|
+
run_pair "go-server + python-client" "$GO_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
294
|
+
|
|
295
|
+
# Rust server combinations
|
|
296
|
+
run_pair "rust-server + go-client" "$RUST_SERVER" -- "$GO_CLIENT"
|
|
297
|
+
run_pair "rust-server + rust-client" "$RUST_SERVER" -- "$RUST_CLIENT"
|
|
298
|
+
run_pair "rust-server + ts-client" "$RUST_SERVER" -- node "$TS_CLIENT"
|
|
299
|
+
run_pair "rust-server + cpp-client" "$RUST_SERVER" -- "$CPP_CLIENT"
|
|
300
|
+
|
|
301
|
+
# TypeScript server combinations
|
|
302
|
+
run_pair "ts-server + go-client" node "$TS_SERVER" -- "$GO_CLIENT"
|
|
303
|
+
run_pair "ts-server + rust-client" node "$TS_SERVER" -- "$RUST_CLIENT"
|
|
304
|
+
run_pair "ts-server + ts-client" node "$TS_SERVER" -- node "$TS_CLIENT"
|
|
305
|
+
run_pair "ts-server + cpp-client" node "$TS_SERVER" -- "$CPP_CLIENT"
|
|
306
|
+
run_pair "ts-server + python-client" node "$TS_SERVER" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
307
|
+
|
|
308
|
+
# Python server combinations
|
|
309
|
+
run_pair "python-server + go-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "$GO_CLIENT"
|
|
310
|
+
run_pair "python-server + ts-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- node "$TS_CLIENT" lifecycle
|
|
311
|
+
run_pair "python-server + python-client" "${PYTHON[@]}" "$SCRIPT_DIR/python-server.py" -- "${PYTHON[@]}" "$SCRIPT_DIR/python-client.py"
|
|
312
|
+
|
|
313
|
+
# C++ server combinations
|
|
314
|
+
run_pair "cpp-server + go-client" "$CPP_SERVER" -- "$GO_CLIENT"
|
|
315
|
+
run_pair "cpp-server + rust-client" "$CPP_SERVER" -- "$RUST_CLIENT"
|
|
316
|
+
run_pair "cpp-server + ts-client" "$CPP_SERVER" -- node "$TS_CLIENT"
|
|
317
|
+
run_pair "cpp-server + cpp-client" "$CPP_SERVER" -- "$CPP_CLIENT"
|
|
318
|
+
fi
|
|
194
319
|
|
|
195
320
|
echo ""
|
|
196
321
|
echo "=== Results: ${PASSED} passed, ${FAILED} failed ==="
|