test_tcp_client.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import asyncio
  2. import pytest
  3. import cq2_sdk.transport.tcp as tcp_module
  4. from cq2_sdk.transport.tcp import TcpClient
  5. from cq2_sdk.transport.framing import WIRE_HEADER_SIZE, decode_header, encode_frame
  6. async def _echo_handler(reader, writer):
  7. try:
  8. while True:
  9. data = await reader.read(4096)
  10. if not data:
  11. break
  12. writer.write(data)
  13. await writer.drain()
  14. finally:
  15. writer.close()
  16. @pytest.fixture
  17. async def echo_server():
  18. server = await asyncio.start_server(_echo_handler, "127.0.0.1", 0)
  19. port = server.sockets[0].getsockname()[1]
  20. yield port
  21. server.close()
  22. await server.wait_closed()
  23. @pytest.mark.asyncio
  24. async def test_send_recv_roundtrip(echo_server):
  25. cli = TcpClient("127.0.0.1", echo_server)
  26. await cli.connect()
  27. try:
  28. await cli.send(msg_id=2001, recog=900000, param1=1, param2=0, param3=0, body=b"hello")
  29. hdr, body = await cli.recv(timeout=2.0)
  30. assert hdr.msg_id == 2001
  31. assert hdr.recog == 900000
  32. assert hdr.param1 == 1
  33. assert body == b"hello"
  34. finally:
  35. await cli.close()
  36. @pytest.mark.asyncio
  37. async def test_send_multiple_frames(echo_server):
  38. cli = TcpClient("127.0.0.1", echo_server)
  39. await cli.connect()
  40. try:
  41. for i in range(3):
  42. await cli.send(msg_id=100 + i, recog=0, param1=i, param2=0, param3=0, body=f"frame{i}".encode())
  43. for i in range(3):
  44. hdr, body = await cli.recv(timeout=2.0)
  45. assert hdr.msg_id == 100 + i
  46. assert body == f"frame{i}".encode()
  47. finally:
  48. await cli.close()
  49. @pytest.mark.asyncio
  50. async def test_recv_timeout_raises():
  51. cli = TcpClient("127.0.0.1", 1) # nothing listening
  52. with pytest.raises(asyncio.TimeoutError):
  53. await cli.recv(timeout=0.3)
  54. @pytest.mark.asyncio
  55. async def test_close_is_idempotent(echo_server):
  56. cli = TcpClient("127.0.0.1", echo_server)
  57. await cli.connect()
  58. await cli.close()
  59. await cli.close() # should not raise
  60. assert cli._closed
  61. @pytest.mark.asyncio
  62. async def test_send_before_connect_raises():
  63. cli = TcpClient("127.0.0.1", 1)
  64. with pytest.raises(RuntimeError, match="not connected"):
  65. await cli.send(1, 0, 0, 0, 0, b"")
  66. @pytest.mark.asyncio
  67. async def test_recv_reassembles_split_wire_header_and_body():
  68. async def handler(reader, writer):
  69. wire = encode_frame(529, 1, 0, 0, 0, b"split", sequence=17)
  70. for chunk in (wire[:11], wire[11:WIRE_HEADER_SIZE], wire[WIRE_HEADER_SIZE:]):
  71. writer.write(chunk)
  72. await writer.drain()
  73. writer.close()
  74. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  75. port = server.sockets[0].getsockname()[1]
  76. client = TcpClient("127.0.0.1", port)
  77. await client.connect()
  78. try:
  79. header, body = await client.recv(timeout=1.0)
  80. assert header.msg_id == 529
  81. assert header.sequence == 17
  82. assert body == b"split"
  83. finally:
  84. await client.close()
  85. server.close()
  86. await server.wait_closed()
  87. @pytest.mark.asyncio
  88. async def test_send_uses_and_increments_initial_sequence():
  89. received: list[int] = []
  90. complete = asyncio.Event()
  91. async def handler(reader, writer):
  92. for _ in range(2):
  93. header = decode_header(await reader.readexactly(WIRE_HEADER_SIZE))
  94. if header.body_len:
  95. await reader.readexactly(header.body_len)
  96. received.append(header.sequence)
  97. complete.set()
  98. writer.close()
  99. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  100. port = server.sockets[0].getsockname()[1]
  101. client = TcpClient("127.0.0.1", port, initial_sequence=9)
  102. await client.connect()
  103. try:
  104. await client.send(2001, 0, 0, 0, 0, b"first")
  105. await client.send(2002, 0, 0, 0, 0, b"second")
  106. await asyncio.wait_for(complete.wait(), timeout=1.0)
  107. assert received == [9, 10]
  108. finally:
  109. await client.close()
  110. server.close()
  111. await server.wait_closed()
  112. @pytest.mark.asyncio
  113. async def test_send_uses_current_sdata():
  114. received = asyncio.Future()
  115. async def handler(reader, writer):
  116. header = decode_header(await reader.readexactly(WIRE_HEADER_SIZE))
  117. if header.body_len:
  118. await reader.readexactly(header.body_len)
  119. received.set_result(header)
  120. writer.close()
  121. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  122. port = server.sockets[0].getsockname()[1]
  123. client = TcpClient("127.0.0.1", port)
  124. client.set_sdata(149)
  125. await client.connect()
  126. try:
  127. await client.send(2001, 900000, 1, 2, 3, b"login")
  128. header = await asyncio.wait_for(received, timeout=1.0)
  129. assert header.param3 == 3
  130. assert header.sdata == 149
  131. finally:
  132. await client.close()
  133. server.close()
  134. await server.wait_closed()
  135. @pytest.mark.asyncio
  136. async def test_reconnect_resets_sequence_and_preserves_sdata():
  137. received: list[tuple[int, int]] = []
  138. complete = asyncio.Event()
  139. async def handler(reader, writer):
  140. header = decode_header(await reader.readexactly(WIRE_HEADER_SIZE))
  141. if header.body_len:
  142. await reader.readexactly(header.body_len)
  143. received.append((header.sequence, header.sdata))
  144. if len(received) == 2:
  145. complete.set()
  146. writer.close()
  147. await writer.wait_closed()
  148. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  149. port = server.sockets[0].getsockname()[1]
  150. client = TcpClient("127.0.0.1", port)
  151. client.set_sdata(149)
  152. await client.connect()
  153. try:
  154. await client.send(100, 0, 0, 0, 0, b"role")
  155. await client.reconnect("127.0.0.1", port)
  156. await client.send(20, 0, 0, 0, 0, b"world")
  157. await asyncio.wait_for(complete.wait(), timeout=1.0)
  158. assert received == [(0, 149), (0, 149)]
  159. finally:
  160. await client.close()
  161. server.close()
  162. await server.wait_closed()
  163. @pytest.mark.asyncio
  164. async def test_heartbeat_uses_current_unix_time_in_recog(monkeypatch):
  165. received = asyncio.Future()
  166. async def handler(reader, writer):
  167. header = decode_header(await reader.readexactly(WIRE_HEADER_SIZE))
  168. received.set_result(header)
  169. writer.close()
  170. class FixedTime:
  171. @staticmethod
  172. def time():
  173. return 1_789_709_744
  174. monkeypatch.setattr(tcp_module, "time", FixedTime, raising=False)
  175. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  176. port = server.sockets[0].getsockname()[1]
  177. client = TcpClient("127.0.0.1", port)
  178. client.set_sdata(149)
  179. await client.connect()
  180. try:
  181. await client.start_heartbeat(interval=0.01)
  182. header = await asyncio.wait_for(received, timeout=1.0)
  183. assert header.msg_id == 60000
  184. assert header.recog == 1_789_709_744
  185. assert header.sdata == 149
  186. finally:
  187. await client.close()
  188. server.close()
  189. await server.wait_closed()
  190. @pytest.mark.asyncio
  191. async def test_recv_surfaces_unsupported_body_codec():
  192. async def handler(reader, writer):
  193. writer.write(encode_frame(529, 0, 0, 0, 0, b"compressed", codec=1))
  194. await writer.drain()
  195. writer.close()
  196. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  197. port = server.sockets[0].getsockname()[1]
  198. client = TcpClient("127.0.0.1", port)
  199. await client.connect()
  200. try:
  201. with pytest.raises(ValueError, match="unsupported body codec: 1"):
  202. await client.recv(timeout=1.0)
  203. finally:
  204. await client.close()
  205. server.close()
  206. await server.wait_closed()
  207. @pytest.mark.asyncio
  208. async def test_recv_delivers_queued_frame_before_later_codec_error():
  209. async def handler(reader, writer):
  210. writer.write(encode_frame(529, 0, 0, 0, 0, b"valid"))
  211. writer.write(encode_frame(530, 0, 0, 0, 0, b"compressed", codec=1))
  212. await writer.drain()
  213. writer.close()
  214. server = await asyncio.start_server(handler, "127.0.0.1", 0)
  215. port = server.sockets[0].getsockname()[1]
  216. client = TcpClient("127.0.0.1", port)
  217. await client.connect()
  218. try:
  219. for _ in range(100):
  220. if client._recv_error is not None:
  221. break
  222. await asyncio.sleep(0.01)
  223. assert client._recv_error is not None
  224. header, body = await client.recv(timeout=1.0)
  225. assert header.msg_id == 529
  226. assert body == b"valid"
  227. with pytest.raises(ValueError, match="unsupported body codec: 1"):
  228. await client.recv(timeout=1.0)
  229. finally:
  230. await client.close()
  231. server.close()
  232. await server.wait_closed()