Skip to content

Commit fb9682a

Browse files
committed
feat: cross language integration tests
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent 48cb836 commit fb9682a

File tree

15 files changed

+1641
-3
lines changed

15 files changed

+1641
-3
lines changed

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ starpc-*.tgz
3434
# Rust
3535
/target/
3636
Cargo.lock
37-
.opencode/tool-output/
37+
.opencode/tool-output/
38+
39+
# Cross-language integration build artifacts
40+
integration/cross-language/go-server/go-server
41+
integration/cross-language/go-client/go-client
42+
integration/cross-language/*.mjs
43+
integration/cross-language/*.mjs.map
44+
/go-client
45+
/go-server
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Cross-Language Integration Testing Infrastructure
2+
3+
## Overview
4+
Starpc includes a comprehensive cross-language integration testing framework that validates RPC protocol compatibility across Go, TypeScript, Rust, and C++. The framework tests 12 total cross-language combinations, with every server paired with every other language's client.
5+
6+
## Architecture
7+
8+
### Directory Structure
9+
- `integration/cross-language/` - Main orchestration directory
10+
- `integration/cross-language/run.bash` - Orchestration script that starts servers and runs clients
11+
- `integration/cross-language/cpp-server.cpp` - C++ server implementation
12+
- `integration/cross-language/cpp-client.cpp` - C++ client implementation
13+
- `echo/integration_server.rs` - Rust server implementation
14+
- `echo/integration_client.rs` - Rust client implementation
15+
- `integration/cross-language/go-server.go` - Go server
16+
- `integration/cross-language/go-client.go` - Go client
17+
- `integration/cross-language/ts-server.ts` - TypeScript server (transpiled to ts-server.mjs)
18+
- `integration/cross-language/ts-client.ts` - TypeScript client (transpiled to ts-client.mjs)
19+
20+
## Protocol Specification
21+
22+
### Transport Layer
23+
- Raw TCP connections, one RPC per TCP connection
24+
- 4-byte little-endian (LE) uint32 length prefix before each message
25+
- Server startup behavior: All servers print "LISTENING 127.0.0.1:<port>" to stdout
26+
27+
### Server Binaries
28+
- Go: `go-server`
29+
- TypeScript: `ts-server.ts``ts-server.mjs` (via esbuild)
30+
- Rust: `integration-server` (built from echo/Cargo.toml)
31+
- C++: `cpp-server` (built from integration/cross-language/cpp-server.cpp)
32+
33+
### Client Binaries
34+
- Go: `go-client`
35+
- TypeScript: `ts-client.ts``ts-client.mjs` (via esbuild)
36+
- Rust: `integration-client` (built from echo/Cargo.toml)
37+
- C++: `cpp-client` (built from integration/cross-language/cpp-client.cpp)
38+
39+
## Test Patterns
40+
41+
The framework validates four core RPC patterns:
42+
1. **Unary**: Single request-response
43+
2. **ServerStream**: Server sends 5 messages in sequence
44+
3. **ClientStream**: Client sends stream of messages
45+
4. **BidiStream**: Bidirectional streaming; server sends "hello from server" as initial message
46+
47+
## Build Process
48+
49+
### Go
50+
```bash
51+
go build -o go-server ./integration/cross-language/go-server.go
52+
go build -o go-client ./integration/cross-language/go-client.go
53+
```
54+
55+
### TypeScript
56+
- Built via esbuild to standalone .mjs files
57+
- Executed with `node` or `bun`
58+
59+
### Rust
60+
- Bins defined in `echo/Cargo.toml`
61+
- Built with `cargo build --release`
62+
- Binaries: `target/release/integration-server`, `target/release/integration-client`
63+
64+
### C++
65+
- CMakeLists.txt targets: `cpp-integration-server`, `cpp-integration-client`
66+
- Built via cmake
67+
68+
## Running Tests
69+
70+
### Manual Execution
71+
```bash
72+
cd integration/cross-language
73+
bash run.bash
74+
```
75+
76+
### npm/bun Script
77+
```bash
78+
bun run test:cross-language
79+
```
80+
81+
### CI Integration
82+
- Added to `.github/workflows/tests.yml`
83+
- Runs as part of automated test suite
84+
85+
## Cross-Language Combinations Tested
86+
87+
All 12 combinations:
88+
- Go server ↔ TS, Rust, C++ clients
89+
- TS server ↔ Go, Rust, C++ clients
90+
- Rust server ↔ Go, TS, C++ clients
91+
- C++ server ↔ Go, TS, Rust clients
92+
93+
## Configuration References
94+
95+
### Cargo.toml Binaries (echo/)
96+
- `integration-server`
97+
- `integration-client`
98+
99+
### CMakeLists.txt Targets
100+
- `cpp-integration-server`
101+
- `cpp-integration-client`

CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ if(STARPC_BUILD_TESTS)
109109
)
110110

111111
add_test(NAME echo_e2e_test COMMAND echo_e2e_test)
112+
113+
# Cross-language integration test server
114+
add_executable(cpp-integration-server
115+
integration/cross-language/cpp-server.cpp
116+
echo/echo_srpc.pb.cpp
117+
)
118+
target_link_libraries(cpp-integration-server PRIVATE
119+
starpc
120+
echo_proto
121+
)
122+
123+
# Cross-language integration test client
124+
add_executable(cpp-integration-client
125+
integration/cross-language/cpp-client.cpp
126+
echo/echo_srpc.pb.cpp
127+
)
128+
target_link_libraries(cpp-integration-client PRIVATE
129+
starpc
130+
echo_proto
131+
)
112132
endif()
113133

114134
# Install targets

echo/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ publish = false
1010
name = "echo-example"
1111
path = "main.rs"
1212

13+
[[bin]]
14+
name = "integration-server"
15+
path = "integration_server.rs"
16+
17+
[[bin]]
18+
name = "integration-client"
19+
path = "integration_client.rs"
20+
1321
[dependencies]
1422
starpc = { workspace = true }
1523
prost = { workspace = true }

echo/integration_client.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
mod gen;
2+
3+
use std::sync::Arc;
4+
5+
use async_trait::async_trait;
6+
use starpc::client::{OpenStream, PacketReceiver, SrpcClient};
7+
use starpc::rpc::PacketWriter;
8+
use starpc::transport::create_packet_channel;
9+
use starpc::{Error, Result};
10+
// Use Error::Remote for test assertion errors since there's no Error::Remote.
11+
use tokio::net::TcpStream;
12+
13+
use gen::{EchoMsg, EchoerClient, EchoerClientImpl};
14+
15+
const BODY_TXT: &str = "hello world via starpc cross-language e2e test";
16+
17+
/// Opens a new TCP connection per RPC call.
18+
struct TcpStreamOpener {
19+
addr: String,
20+
}
21+
22+
impl TcpStreamOpener {
23+
fn new(addr: String) -> Self {
24+
Self { addr }
25+
}
26+
}
27+
28+
#[async_trait]
29+
impl OpenStream for TcpStreamOpener {
30+
async fn open_stream(&self) -> Result<(Arc<dyn PacketWriter>, PacketReceiver)> {
31+
let stream = TcpStream::connect(&self.addr).await?;
32+
let (read, write) = tokio::io::split(stream);
33+
Ok(create_packet_channel(read, write))
34+
}
35+
}
36+
37+
#[tokio::main]
38+
async fn main() {
39+
let addr = std::env::args()
40+
.nth(1)
41+
.expect("usage: integration-client <addr>");
42+
43+
let opener = TcpStreamOpener::new(addr);
44+
let client = SrpcClient::new(opener);
45+
let echo = EchoerClientImpl::new(client);
46+
47+
if let Err(e) = test_unary(&echo).await {
48+
eprintln!("unary test failed: {}", e);
49+
std::process::exit(1);
50+
}
51+
52+
if let Err(e) = test_server_stream(&echo).await {
53+
eprintln!("server stream test failed: {}", e);
54+
std::process::exit(1);
55+
}
56+
57+
if let Err(e) = test_client_stream(&echo).await {
58+
eprintln!("client stream test failed: {}", e);
59+
std::process::exit(1);
60+
}
61+
62+
if let Err(e) = test_bidi_stream(&echo).await {
63+
eprintln!("bidi stream test failed: {}", e);
64+
std::process::exit(1);
65+
}
66+
67+
println!("All tests passed.");
68+
}
69+
70+
async fn test_unary(echo: &dyn EchoerClient) -> Result<()> {
71+
println!("Testing Unary RPC...");
72+
let req = EchoMsg {
73+
body: BODY_TXT.to_string(),
74+
};
75+
let resp = echo.echo(&req).await?;
76+
if resp.body != BODY_TXT {
77+
return Err(Error::Remote(format!(
78+
"expected {:?} got {:?}",
79+
BODY_TXT, resp.body
80+
)));
81+
}
82+
println!(" PASSED");
83+
Ok(())
84+
}
85+
86+
async fn test_server_stream(echo: &dyn EchoerClient) -> Result<()> {
87+
println!("Testing ServerStream RPC...");
88+
let req = EchoMsg {
89+
body: BODY_TXT.to_string(),
90+
};
91+
let stream = echo.echo_server_stream(&req).await?;
92+
let mut received = 0;
93+
loop {
94+
match stream.recv().await {
95+
Ok(msg) => {
96+
if msg.body != BODY_TXT {
97+
return Err(Error::Remote(format!(
98+
"expected {:?} got {:?}",
99+
BODY_TXT, msg.body
100+
)));
101+
}
102+
received += 1;
103+
}
104+
Err(Error::StreamClosed) => break,
105+
Err(e) => return Err(e),
106+
}
107+
}
108+
if received != 5 {
109+
return Err(Error::Remote(format!(
110+
"expected 5 messages, got {}",
111+
received
112+
)));
113+
}
114+
println!(" PASSED");
115+
Ok(())
116+
}
117+
118+
async fn test_client_stream(echo: &dyn EchoerClient) -> Result<()> {
119+
println!("Testing ClientStream RPC...");
120+
let stream = echo.echo_client_stream().await?;
121+
stream
122+
.send(&EchoMsg {
123+
body: BODY_TXT.to_string(),
124+
})
125+
.await?;
126+
let resp = stream.close_and_recv().await?;
127+
if resp.body != BODY_TXT {
128+
return Err(Error::Remote(format!(
129+
"expected {:?} got {:?}",
130+
BODY_TXT, resp.body
131+
)));
132+
}
133+
println!(" PASSED");
134+
Ok(())
135+
}
136+
137+
async fn test_bidi_stream(echo: &dyn EchoerClient) -> Result<()> {
138+
println!("Testing BidiStream RPC...");
139+
let stream = echo.echo_bidi_stream().await?;
140+
141+
// Receive initial message from server.
142+
let msg = stream.recv().await?;
143+
if msg.body != "hello from server" {
144+
return Err(Error::Remote(format!(
145+
"expected {:?} got {:?}",
146+
"hello from server", msg.body
147+
)));
148+
}
149+
150+
// Send a message and expect echo.
151+
stream
152+
.send(&EchoMsg {
153+
body: BODY_TXT.to_string(),
154+
})
155+
.await?;
156+
let resp = stream.recv().await?;
157+
if resp.body != BODY_TXT {
158+
return Err(Error::Remote(format!(
159+
"expected {:?} got {:?}",
160+
BODY_TXT, resp.body
161+
)));
162+
}
163+
164+
stream.close().await?;
165+
println!(" PASSED");
166+
Ok(())
167+
}

0 commit comments

Comments
 (0)