返回教程正文

配套源码

runtime_edge_cases.py

app/tests/runtime_edge_cases.py
ros_gateway Brick:在 App Lab 中建立可靠的 WebSocket 通道app/tests/runtime_edge_cases.py
Python182 行
  1. # SPDX-License-Identifier: MIT
  2. import argparse
  3. from contextlib import contextmanager
  4. import json
  5. import time
  6. from websockets.exceptions import ConnectionClosed
  7. from websockets.sync.client import connect
  8. def send_json(websocket, message):
  9. """
  10. @description : 向服务端发送紧凑 JSON 文本
  11. @param websocket : 已建立的 WebSocket 客户端连接
  12. @param message : 待发送消息字典
  13. @return : 无返回值
  14. """
  15. websocket.send(json.dumps(message, separators=(",", ":")))
  16. def receive_json(websocket, timeout=3.0):
  17. """
  18. @description : 接收并解析一条服务端 JSON 消息
  19. @param websocket : 已建立的 WebSocket 客户端连接
  20. @param timeout : 接收超时秒数
  21. @return : 解析后的消息字典
  22. """
  23. return json.loads(websocket.recv(timeout=timeout))
  24. @contextmanager
  25. def open_owner(url, node):
  26. """
  27. @description : 建立连接并完成 ROS 2 客户端握手
  28. @param url : WebSocket 服务地址
  29. @param node : 测试客户端节点名称
  30. @return : 产生已完成握手连接的上下文管理器
  31. """
  32. with connect(url, open_timeout=3.0, close_timeout=2.0) as websocket:
  33. send_json(
  34. websocket,
  35. {
  36. "version": 1,
  37. "type": "hello",
  38. "role": "ros2",
  39. "node": node,
  40. },
  41. )
  42. hello = receive_json(websocket)
  43. if hello.get("type") != "hello" or hello.get("role") != "app":
  44. raise RuntimeError("server hello was not received")
  45. yield websocket
  46. def test_wrong_path(base_url):
  47. """
  48. @description : 验证非 /ros 路径会被策略关闭
  49. @param base_url : 正确 WebSocket 服务地址
  50. @return : 关闭码为 1008 时返回 True
  51. """
  52. wrong_url = base_url.rsplit("/", 1)[0] + "/wrong"
  53. try:
  54. with connect(wrong_url, open_timeout=3.0, close_timeout=2.0) as websocket:
  55. websocket.recv(timeout=2.0)
  56. except ConnectionClosed as exc:
  57. return exc.code == 1008
  58. return False
  59. def test_protocol_errors(url):
  60. """
  61. @description : 验证非法 JSON 与非递增序号返回结构化错误
  62. @param url : WebSocket 服务地址
  63. @return : 两个错误路径均正确时返回 True
  64. """
  65. with open_owner(url, "edge_case_protocol") as websocket:
  66. websocket.send("{")
  67. invalid_json = receive_json(websocket)
  68. send_json(
  69. websocket,
  70. {
  71. "version": 1,
  72. "type": "heartbeat",
  73. "seq": 10,
  74. "timestamp_ms": time.time_ns() // 1_000_000,
  75. },
  76. )
  77. send_json(
  78. websocket,
  79. {
  80. "version": 1,
  81. "type": "heartbeat",
  82. "seq": 10,
  83. "timestamp_ms": time.time_ns() // 1_000_000,
  84. },
  85. )
  86. deadline = time.monotonic() + 3.0
  87. sequence_error = None
  88. while time.monotonic() < deadline:
  89. message = receive_json(websocket, max(0.01, deadline - time.monotonic()))
  90. if message.get("type") == "error" and message.get("code") == "non_monotonic_seq":
  91. sequence_error = message
  92. break
  93. return (
  94. invalid_json.get("type") == "error"
  95. and invalid_json.get("code") == "invalid_json"
  96. and sequence_error is not None
  97. )
  98. def test_single_owner(url):
  99. """
  100. @description : 验证第二个客户端不能抢占已握手客户端
  101. @param url : WebSocket 服务地址
  102. @return : 第二连接收到 1013 关闭码时返回 True
  103. """
  104. with open_owner(url, "edge_case_owner"):
  105. try:
  106. with connect(url, open_timeout=3.0, close_timeout=2.0) as contender:
  107. contender.recv(timeout=2.0)
  108. except ConnectionClosed as exc:
  109. return exc.code == 1013
  110. return False
  111. def test_heartbeat_timeout_and_reconnect(url):
  112. """
  113. @description : 验证无应用层心跳会断开,且随后能够重新握手
  114. @param url : WebSocket 服务地址
  115. @return : 超时关闭和再次握手均成功时返回 True
  116. """
  117. timeout_closed = False
  118. with open_owner(url, "edge_case_timeout") as websocket:
  119. deadline = time.monotonic() + 5.0
  120. while time.monotonic() < deadline:
  121. try:
  122. websocket.recv(timeout=max(0.01, deadline - time.monotonic()))
  123. except ConnectionClosed as exc:
  124. timeout_closed = exc.code == 1008
  125. break
  126. with open_owner(url, "edge_case_reconnect"):
  127. reconnected = True
  128. return timeout_closed and reconnected
  129. def run(url):
  130. """
  131. @description : 顺序执行 WebSocket 运行时边界测试
  132. @param url : WebSocket 服务地址
  133. @return : 测试名称到布尔结果的字典
  134. """
  135. return {
  136. "wrong_path_rejected": test_wrong_path(url),
  137. "protocol_errors": test_protocol_errors(url),
  138. "single_owner": test_single_owner(url),
  139. "heartbeat_timeout_and_reconnect": test_heartbeat_timeout_and_reconnect(url),
  140. }
  141. def main():
  142. """
  143. @description : 解析命令行参数并根据边界测试结果设置退出码
  144. @param : 无参数
  145. @return : 无返回值
  146. """
  147. parser = argparse.ArgumentParser(description="Test ROS Gateway runtime edge cases")
  148. parser.add_argument("--url", default="ws://127.0.0.1:8765/ros")
  149. arguments = parser.parse_args()
  150. results = run(arguments.url)
  151. print(json.dumps(results, ensure_ascii=False, indent=2))
  152. if not all(results.values()):
  153. raise SystemExit(1)
  154. if __name__ == "__main__":
  155. main()