Skip to content

Commit 1ce9a1f

Browse files
committed
test(transport): add tests for utf-8 handling in stdio transport
Add test cases to verify proper UTF-8 character handling in StdioClientTransport when system default charset is not UTF-8. Includes a test echo server subprocess to simulate non-UTF-8 environment.
1 parent 497d6f1 commit 1ce9a1f

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import io.modelcontextprotocol.json.McpJsonDefaults;
8+
import io.modelcontextprotocol.spec.McpSchema;
9+
import org.jspecify.annotations.NonNull;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.Timeout;
12+
import reactor.test.StepVerifier;
13+
14+
import java.io.BufferedReader;
15+
import java.io.InputStreamReader;
16+
import java.nio.charset.StandardCharsets;
17+
import java.nio.file.FileSystems;
18+
import java.util.Map;
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.TimeUnit;
21+
import java.util.concurrent.atomic.AtomicReference;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* Tests for {@link StdioClientTransport}.
27+
*
28+
* @author Christian Tzolov
29+
*/
30+
@Timeout(30)
31+
class StdioClientTransportTests {
32+
33+
static final String FILE_SEPARATOR = FileSystems.getDefault().getSeparator();
34+
35+
@Test
36+
void shouldHandleUtf8MessagesWithNonUtf8DefaultCharset() throws Exception {
37+
String utf8Content = "한글 漢字 café 🎉";
38+
39+
String javaHome = System.getProperty("java.home");
40+
String classpath = System.getProperty("java.class.path");
41+
String javaExecutable = javaHome + FILE_SEPARATOR + "bin" + FILE_SEPARATOR + "java";
42+
43+
ServerParameters params = ServerParameters.builder(javaExecutable)
44+
.args("-Dfile.encoding=ISO-8859-1", "-cp", classpath, StdioUtf8TestEchoServer.class.getName())
45+
.build();
46+
47+
StdioClientTransport transport = new StdioClientTransport(params, McpJsonDefaults.getMapper());
48+
49+
AtomicReference<McpSchema.JSONRPCMessage> receivedMessage = new AtomicReference<>();
50+
CountDownLatch messageLatch = new CountDownLatch(1);
51+
52+
StepVerifier.create(transport.connect(message -> {
53+
return message.doOnNext(msg -> {
54+
receivedMessage.set(msg);
55+
messageLatch.countDown();
56+
});
57+
})).verifyComplete();
58+
59+
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "echo", 1,
60+
Map.of("message", utf8Content));
61+
62+
StepVerifier.create(transport.sendMessage(request)).verifyComplete();
63+
64+
assertThat(messageLatch.await(10, TimeUnit.SECONDS)).isTrue();
65+
66+
assertThat(receivedMessage.get()).isNotNull();
67+
assertThat(receivedMessage.get()).isInstanceOf(McpSchema.JSONRPCResponse.class);
68+
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) receivedMessage.get();
69+
assertThat(response.result()).isEqualTo(utf8Content);
70+
71+
transport.closeGracefully().block();
72+
}
73+
74+
@Test
75+
void shouldHandleUtf8ErrorMessagesWithNonUtf8DefaultCharset() throws Exception {
76+
String utf8ErrorContent = "错误: 한글 漢字 🎉";
77+
78+
String javaHome = System.getProperty("java.home");
79+
String classpath = System.getProperty("java.class.path");
80+
String javaExecutable = javaHome + FILE_SEPARATOR + "bin" + FILE_SEPARATOR + "java";
81+
82+
ProcessBuilder pb = new ProcessBuilder(javaExecutable, "-Dfile.encoding=ISO-8859-1", "-cp", classpath,
83+
StdioUtf8TestEchoServer.class.getName());
84+
pb.redirectErrorStream(false);
85+
86+
Process process = pb.start();
87+
88+
try {
89+
process.getOutputStream()
90+
.write(("{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{\"message\":\"test\"},\"id\":1}\n")
91+
.getBytes(StandardCharsets.UTF_8));
92+
process.getOutputStream().flush();
93+
94+
Thread errorThread = getErrorThread(process, utf8ErrorContent);
95+
96+
process.waitFor(10, TimeUnit.SECONDS);
97+
errorThread.join(1000);
98+
}
99+
finally {
100+
process.destroyForcibly();
101+
process.waitFor(10, TimeUnit.SECONDS);
102+
}
103+
}
104+
105+
private static @NonNull Thread getErrorThread(Process process, String utf8ErrorContent) {
106+
AtomicReference<String> errorContent = new AtomicReference<>();
107+
CountDownLatch errorLatch = new CountDownLatch(1);
108+
109+
Thread errorThread = new Thread(() -> {
110+
try (BufferedReader errorReader = new BufferedReader(
111+
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
112+
String line;
113+
while ((line = errorReader.readLine()) != null) {
114+
if (line.contains(utf8ErrorContent)) {
115+
errorContent.set(line);
116+
errorLatch.countDown();
117+
break;
118+
}
119+
}
120+
}
121+
catch (Exception ignored) {
122+
}
123+
});
124+
errorThread.start();
125+
return errorThread;
126+
}
127+
128+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import java.io.BufferedReader;
8+
import java.io.InputStreamReader;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.TimeUnit;
12+
13+
/**
14+
* Minimal STDIO echo server for testing UTF-8 encoding behavior in StdioClientTransport.
15+
*
16+
* <p>
17+
* This class is spawned as a subprocess with {@code -Dfile.encoding=ISO-8859-1} to
18+
* simulate a non-UTF-8 default charset environment. It reads JSON-RPC messages from stdin
19+
* and echoes the {@code params.message} value back to stdout, allowing the parent test to
20+
* verify that multi-byte UTF-8 characters are preserved.
21+
*
22+
* @see StdioClientTransportTests#shouldHandleUtf8MessagesWithNonUtf8DefaultCharset
23+
*/
24+
public class StdioUtf8TestEchoServer {
25+
26+
public static void main(String[] args) throws Exception {
27+
CountDownLatch latch = new CountDownLatch(1);
28+
StringBuilder receivedMessage = new StringBuilder();
29+
30+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
31+
String line;
32+
while ((line = reader.readLine()) != null) {
33+
if (line.contains("\"echo\"")) {
34+
int start = line.indexOf("\"message\":\"") + "\"message\":\"".length();
35+
int end = line.indexOf("\"", start);
36+
if (start > 0 && end > start) {
37+
receivedMessage.append(line, start, end);
38+
}
39+
String response = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + receivedMessage + "\"}\n";
40+
System.out.write(response.getBytes(StandardCharsets.UTF_8));
41+
System.out.flush();
42+
latch.countDown();
43+
break;
44+
}
45+
}
46+
}
47+
48+
latch.await(5, TimeUnit.SECONDS);
49+
}
50+
51+
}

0 commit comments

Comments
 (0)