/* * simcu::websocket.tests —— 端到端集成测试(cjpm test)。 * 覆盖:文本回声 / 广播 / 关闭握手(客户端主动、服务端主动)/ terminate / * ping 心跳后连接仍可用 / 路径路由(多 handler 按注册路径)/ maxPayload 超限(1009) / * 组播(group.send)/ 单播(conn.send)。 * * 运行:在 websocket-cj 目录执行 cjpm test */ package simcu::websocket.tests import std.time.* import std.unittest.* import std.unittest.testmacro.Expect import std.unittest.testmacro.TestCase import std.unittest.testmacro.Test import simcu::websocket.client.WebSocketClient import simcu::websocket.common.MessageType import simcu::websocket.common.WebSocketException import simcu::websocket.common.WebSocketMessage import simcu::websocket.server.IWebsocketHandler import simcu::websocket.server.WebSocketConnection import simcu::websocket.server.WebSocketServer /// 测试回收集合:把回调中捕获的值存到独立实例字段,避免闭包捕获歧义。 class WsTestHarness { var serverConn: ?WebSocketConnection = None var connMessage: ?String = None var connErrorCode: Int64 = 0 var connCloseCode: Int64 = -1 var connCloseReason: String = "" var clientMessage: ?String = None var clientCloseCode: Int64 = -1 var clientCloseReason: String = "" var clientErrorCount: Int64 = 0 } /// 轮询等待条件成立(每 50ms 检查一次)。 private func waitUntil(timeoutMs: Int64, condition: () -> Bool): Bool { let total = timeoutMs / 50 for (_ in 0..total) { if (condition()) { return true } sleep(Duration.millisecond * 50) } condition() } /// 测试用 handler:把事件回调逻辑收进接口方法,行为写入 harness。 class TestWsHandler <: IWebsocketHandler { let harness: WsTestHarness init(harness: WsTestHarness) { this.harness = harness } public override func onConnect(conn: WebSocketConnection): Unit { harness.serverConn = Some(conn) } public override func onMessage(conn: WebSocketConnection, msg: WebSocketMessage): Unit { harness.connMessage = msg.text.getOrThrow() conn.send("echo: ${msg.text.getOrThrow()}") } public override func onClose(_: WebSocketConnection, code: Int64, reason: String): Unit { harness.connCloseCode = code harness.connCloseReason = reason } public override func onError(_: WebSocketConnection, e: Exception): Unit { if (let we: WebSocketException <- e) { harness.connErrorCode = we.code } } } /// 广播场景:harness1 收到广播后转发文本。 class EchoHandler <: IWebsocketHandler { let harness: WsTestHarness init(harness: WsTestHarness) { this.harness = harness } public override func onConnect(_: WebSocketConnection): Unit { () } public override func onMessage(_: WebSocketConnection, msg: WebSocketMessage): Unit { harness.connMessage = msg.text.getOrThrow() } public override func onClose(_: WebSocketConnection, _: Int64, _: String): Unit { () } public override func onError(_: WebSocketConnection, _: Exception): Unit { () } } /// 服务端主动关闭场景:连接建立后立即 close。 class CloseOnConnectHandler <: IWebsocketHandler { init() {} public override func onConnect(conn: WebSocketConnection): Unit { conn.close(code: 4000, reason: "svr") } public override func onMessage(_: WebSocketConnection, _: WebSocketMessage): Unit { () } public override func onClose(_: WebSocketConnection, _: Int64, _: String): Unit { () } public override func onError(_: WebSocketConnection, _: Exception): Unit { () } } /// 分组场景:连接建立后加入 "room" 组,便于验证组播/单播。 class GroupJoinHandler <: IWebsocketHandler { public override func onConnect(conn: WebSocketConnection): Unit { conn.joinGroup("room") } public override func onMessage(_: WebSocketConnection, _: WebSocketMessage): Unit { () } public override func onClose(_: WebSocketConnection, _: Int64, _: String): Unit { () } public override func onError(_: WebSocketConnection, _: Exception): Unit { () } } @Test public class WebSocketTest { /// 1. 文本回声:client.send → server 收到后回发 → client 收到 @TestCase public func echoText(): Unit { let h = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => TestWsHandler(h) } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.onMessage = { m: WebSocketMessage => h.clientMessage = m.text.getOrThrow() } client.connect() client.send("hello") @Expect(waitUntil(5000) { => h.connMessage == Some("hello") }, true) @Expect(waitUntil(5000) { => h.clientMessage == Some("echo: hello") }, true) client.close() server.close() } /// 2. 广播:server.broadcast 两个客户端都收到 @TestCase public func broadcast(): Unit { let h1 = WsTestHarness() let h2 = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => EchoHandler(h1) } server.start() let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c1.onMessage = { m: WebSocketMessage => h1.clientMessage = m.text.getOrThrow() } c1.connect() let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c2.onMessage = { m: WebSocketMessage => h2.clientMessage = m.text.getOrThrow() } c2.connect() @Expect(waitUntil(5000) { => server.connectionCount == 2 }, true) server.broadcast("hi-all") @Expect(waitUntil(5000) { => h1.clientMessage == Some("hi-all") }, true) @Expect(waitUntil(5000) { => h2.clientMessage == Some("hi-all") }, true) c1.close() c2.close() server.close() } /// 3. 关闭握手:client.close(1000, "bye") → 双端 onClose 都收到 (1000, "bye") @TestCase public func closeHandshake(): Unit { let h = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => TestWsHandler(h) } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.onClose = { code: Int64, reason: String => h.clientCloseCode = code h.clientCloseReason = reason } client.connect() client.close(code: 1000, reason: "bye") @Expect(waitUntil(5000) { => h.clientCloseCode == 1000 && h.clientCloseReason == "bye" }, true) @Expect(waitUntil(5000) { => h.connCloseCode == 1000 && h.connCloseReason == "bye" }, true) server.close() } /// 4. 服务端主动关闭:conn.close(4000, "svr") → client onClose 收 (4000, "svr") @TestCase public func serverClose(): Unit { let h = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => CloseOnConnectHandler() } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.onClose = { code: Int64, reason: String => h.clientCloseCode = code h.clientCloseReason = reason } client.connect() @Expect(waitUntil(5000) { => h.clientCloseCode == 4000 && h.clientCloseReason == "svr" }, true) server.close() } /// 5. terminate:client.terminate() 立即断开 → server conn onClose 收 (1006, "") @TestCase public func terminate(): Unit { let h = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => TestWsHandler(h) } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.connect() client.terminate() @Expect(waitUntil(5000) { => h.connCloseCode == 1006 && h.connCloseReason == "" }, true) server.close() } /// 6. ping:心跳后连接仍可用,后续消息正常到达 @TestCase public func ping(): Unit { let h = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => TestWsHandler(h) } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.connect() client.ping() sleep(Duration.millisecond * 100) client.send("after-ping") @Expect(waitUntil(5000) { => h.connMessage == Some("after-ping") }, true) client.close() server.close() } /// 7. 路径路由:注册 /ws 与 /admin 两个 handler,/ws 握手成功,/other 被拒(connect 抛异常) @TestCase public func pathRoute(): Unit { let server = WebSocketServer(0) server.addHandler("/ws") { => TestWsHandler(WsTestHarness()) } server.addHandler("/admin") { => TestWsHandler(WsTestHarness()) } server.start() let ok = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") ok.connect() ok.close() let bad = WebSocketClient("127.0.0.1", server.localPort, path: "/other") var rejected = false try { bad.connect() } catch (e: WebSocketException) { rejected = true } @Expect(rejected, true) server.close() } /// 8. maxPayload:server 限制 10 字节,client 发长文本 → server 连接 onError 收 code=1009 @TestCase public func maxPayload(): Unit { let h = WsTestHarness() let server = WebSocketServer(0, maxPayload: 10) server.addHandler("/ws") { => TestWsHandler(h) } server.start() let client = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") client.onError = { e: Exception => h.clientErrorCount += 1 } client.connect() client.send("this message is way too long") @Expect(waitUntil(5000) { => h.connErrorCode == 1009 }, true) client.terminate() server.close() } /// 9. 组播:两个连接加入同组("room"),group("room").send 两连接都收到;关闭一个后组计数减一 @TestCase public func groupMulticast(): Unit { let h1 = WsTestHarness() let h2 = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => GroupJoinHandler() } server.start() let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c1.onMessage = { m: WebSocketMessage => h1.clientMessage = m.text.getOrThrow() } c1.connect() let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c2.onMessage = { m: WebSocketMessage => h2.clientMessage = m.text.getOrThrow() } c2.connect() @Expect(waitUntil(5000) { => server.group("room").count() == 2 }, true) server.group("room").send("team-msg") @Expect(waitUntil(5000) { => h1.clientMessage == Some("team-msg") }, true) @Expect(waitUntil(5000) { => h2.clientMessage == Some("team-msg") }, true) // 关闭一个连接后自动退组,组计数减一 c1.close() @Expect(waitUntil(5000) { => server.group("room").count() == 1 }, true) server.group("room").send("after-leave") @Expect(waitUntil(5000) { => h2.clientMessage == Some("after-leave") }, true) c2.close() server.close() } /// 10. 单播:conn(connId).send 只发到指定连接Id,另一连接收不到 @TestCase public func unicast(): Unit { let h1 = WsTestHarness() let h2 = WsTestHarness() let server = WebSocketServer(0) server.addHandler("/ws") { => GroupJoinHandler() } server.start() let c1 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c1.onMessage = { m: WebSocketMessage => h1.clientMessage = m.text.getOrThrow() } c1.connect() let c2 = WebSocketClient("127.0.0.1", server.localPort, path: "/ws") c2.onMessage = { m: WebSocketMessage => h2.clientMessage = m.text.getOrThrow() } c2.connect() @Expect(waitUntil(5000) { => server.group("room").connIds().size == 2 }, true) h1.clientMessage = None h2.clientMessage = None let ids = server.group("room").connIds() let target = ids[0] server.conn(target).send("only-you") @Expect(waitUntil(5000) { => h1.clientMessage == Some("only-you") || h2.clientMessage == Some("only-you") }, true) // 稍等片刻后断言:仅目标接收,另一连接未收到 sleep(Duration.millisecond * 200) let exactlyOne = (h1.clientMessage == Some("only-you") && h2.clientMessage != Some("only-you")) || (h2.clientMessage == Some("only-you") && h1.clientMessage != Some("only-you")) @Expect(exactlyOne, true) c1.close() c2.close() server.close() } }