Files
opendance-windows/tests/test_pose_server.gd
T

296 lines
8.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends TestCase
## T-WIN-003:WebSocket 关键点接收服务
##
## 这是真实的 socket 端到端测试:在本机起 PoseServer,
## 用 WebSocketPeer 作为客户端连入,走完整的握手 → 二进制帧 → 控制帧流程。
const PoseServer = preload("res://src/net/pose_server.gd")
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
const TEST_PORT := 18765
const SUBPROTOCOL := "opendance.pose.v1"
var _server: PoseServer = null
var _client: WebSocketPeer = null
var _received: Array = []
var _controls: Array = []
var _connected := 0
func _tree() -> SceneTree:
return Engine.get_main_loop() as SceneTree
## 推进若干帧,让服务端与客户端都有机会 poll
func _pump(frames: int = 1) -> void:
for i in frames:
await _tree().process_frame
func _setup() -> void:
_received.clear()
_controls.clear()
_connected = 0
_server = PoseServer.new()
_server.subprotocol = SUBPROTOCOL
_tree().root.add_child(_server)
_server.frame_received.connect(func(frame): _received.append(frame))
_server.control_received.connect(func(_id, msg): _controls.append(msg))
_server.client_connected.connect(func(_id, _device): _connected += 1)
_server.listen("127.0.0.1", TEST_PORT)
_client = WebSocketPeer.new()
_client.supported_protocols = PackedStringArray([SUBPROTOCOL])
_client.connect_to_url("ws://127.0.0.1:%d/pose" % TEST_PORT)
# 等待握手完成
var guard := 0
while _client.get_ready_state() == WebSocketPeer.STATE_CONNECTING and guard < 240:
_client.poll()
await _pump()
guard += 1
func _teardown() -> void:
if _client != null:
_client.close()
_client = null
if _server != null:
_server.stop()
_server.queue_free()
_server = null
await _pump(2)
func _sample_points(offset: float = 0.0) -> PackedVector3Array:
var points := PackedVector3Array()
points.resize(33)
for i in 33:
points[i] = Vector3(
clampf(0.5 + offset + float(i % 9) * 0.01, 0.0, 1.0),
clampf(0.5 + float(i % 7) * 0.02, 0.0, 1.0),
float((i % 5) - 2) * 0.1
)
return points
func _sample_confidence() -> PackedFloat32Array:
var conf := PackedFloat32Array()
conf.resize(33)
conf.fill(1.0)
return conf
## 把客户端收到的包取出来
func _drain_client() -> Array:
var out: Array = []
while _client.get_available_packet_count() > 0:
var packet := _client.get_packet()
out.append({
"text": packet.get_string_from_utf8() if _client.was_string_packet() else "",
"binary": packet if not _client.was_string_packet() else PackedByteArray(),
"is_text": _client.was_string_packet(),
})
return out
func test_listen_and_handshake() -> void:
await _setup()
is_true(_server.is_running(), "服务端进入监听状态")
eq(_server.port(), TEST_PORT, "监听端口正确")
eq(_client.get_ready_state(), WebSocketPeer.STATE_OPEN, "客户端握手成功(STATE_OPEN)")
await _pump(3)
eq(_connected, 1, "服务端发出 client_connected 信号")
eq(_server.connected_count(), 1, "已握手连接数为 1")
await _teardown()
func test_subprotocol_is_negotiated() -> void:
await _setup()
eq(_client.get_selected_protocol(), SUBPROTOCOL, "子协议协商为 opendance.pose.v1")
await _teardown()
func test_hello_gets_welcome() -> void:
await _setup()
_client.send_text(JSON.stringify({
"type": "hello",
"protocol": SUBPROTOCOL,
"device": "Pixel 8 (测试)",
"appVersion": "0.1.0",
"cameraFacing": "front",
"targetFps": 30,
"resolution": "640x480",
}))
var got_welcome := false
var welcome: Dictionary = {}
for i in 60:
_client.poll()
await _pump()
for packet in _drain_client():
if packet["is_text"]:
var parsed: Variant = JSON.parse_string(packet["text"])
if typeof(parsed) == TYPE_DICTIONARY and str((parsed as Dictionary).get("type", "")) == "welcome":
welcome = parsed
got_welcome = true
if got_welcome:
break
is_true(got_welcome, "收到 welcome 控制帧")
eq(str(welcome.get("protocol", "")), SUBPROTOCOL, "welcome 回显协议版本")
ok(welcome.has("sessionId"), "welcome 含 sessionId")
ok(_controls.size() > 0, "服务端发出 control_received 信号")
eq(str(_controls[0].get("device", "")), "Pixel 8 (测试)", "hello 中的设备名被记录")
await _teardown()
func test_binary_frame_is_received_and_decoded() -> void:
await _setup()
var points := _sample_points()
var conf := _sample_confidence()
_client.send(KeypointFrame.encode(1, points, conf, 0))
var got := false
for i in 60:
_client.poll()
await _pump()
if _received.size() > 0:
got = true
break
is_true(got, "服务端收到并解析出关键点帧")
if got:
var frame: KeypointFrame.PoseFrame = _received[0]
eq(frame.frame_id, 1, "frame_id 解析正确")
eq(frame.points.size(), 33, "33 个关键点")
var max_delta := 0.0
for i in 33:
max_delta = maxf(max_delta, absf(frame.points[i].x - points[i].x))
ok(max_delta < 1e-4, "坐标经网络往返后仍在定点精度内", "最大误差 %.8f" % max_delta)
eq(_server.frames_total, 1, "累计帧计数为 1")
eq(_server.malformed_frames, 0, "无畸形帧")
await _teardown()
func test_malformed_frame_is_rejected_without_crashing() -> void:
await _setup()
# 长度错误
_client.send(PackedByteArray([1, 2, 3]))
# magic 错误但长度正确
var bad := PackedByteArray()
bad.resize(239)
_client.send(bad)
# 正常帧,用于确认连接未被破坏
_client.send(KeypointFrame.encode(1, _sample_points(), _sample_confidence(), 0))
var good_arrived := false
for i in 90:
_client.poll()
await _pump()
if _received.size() > 0:
good_arrived = true
break
eq(_server.malformed_frames, 2, "两个畸形帧被拒绝并计数")
is_true(good_arrived, "畸形帧之后连接仍可正常收帧")
eq(_server.frames_total, 1, "仅有效帧计入累计")
await _teardown()
func test_frame_id_gap_is_counted_as_dropped() -> void:
await _setup()
var conf := _sample_confidence()
_client.send(KeypointFrame.encode(1, _sample_points(), conf))
_client.send(KeypointFrame.encode(2, _sample_points(0.01), conf))
# 跳过 3、4、5
_client.send(KeypointFrame.encode(6, _sample_points(0.02), conf))
var guard := 0
while _received.size() < 3 and guard < 120:
_client.poll()
await _pump()
guard += 1
eq(_received.size(), 3, "收到 3 帧")
eq(_server.frames_dropped, 3, "frame_id 跳号被计为丢包(3 帧)")
eq(_server.frames_total, 3, "累计收到 3 帧")
await _teardown()
func test_upper_body_and_mirror_flags_survive_transport() -> void:
await _setup()
var flags := KeypointFrame.FLAG_MIRRORED | KeypointFrame.FLAG_UPPER_BODY_ONLY
_client.send(KeypointFrame.encode(7, _sample_points(), _sample_confidence(), flags))
var guard := 0
while _received.is_empty() and guard < 90:
_client.poll()
await _pump()
guard += 1
if _received.is_empty():
ok(false, "未收到帧")
else:
var frame: KeypointFrame.PoseFrame = _received[0]
is_true(frame.is_mirrored(), "镜像标志经网络传输保留")
is_true(frame.is_upper_body_only(), "上半身模式标志经网络传输保留")
await _teardown()
func test_config_broadcast_reaches_client() -> void:
await _setup()
await _pump(3)
_server.mirror = true
_server.expected_fps = 24
_server.broadcast_config()
var config: Dictionary = {}
for i in 60:
_client.poll()
await _pump()
for packet in _drain_client():
if packet["is_text"]:
var parsed: Variant = JSON.parse_string(packet["text"])
if typeof(parsed) == TYPE_DICTIONARY and str((parsed as Dictionary).get("type", "")) == "config":
config = parsed
if not config.is_empty():
break
is_false(config.is_empty(), "客户端收到 config 控制帧")
eq(bool(config.get("mirror", false)), true, "config.mirror 正确")
eq(int(config.get("targetFps", 0)), 24, "config.targetFps 正确")
ok(config.has("upperBodyOnly"), "config 含 upperBodyOnly")
ok(config.has("resolution"), "config 含 resolution")
await _teardown()
func test_snapshot_reports_state() -> void:
await _setup()
await _pump(3)
var snap := _server.snapshot()
is_true(bool(snap["running"]), "snapshot 报告运行中")
eq(int(snap["port"]), TEST_PORT, "snapshot 报告端口")
eq(int(snap["connected"]), 1, "snapshot 报告已连接 1 个客户端")
await _teardown()
func test_stop_closes_listener() -> void:
await _setup()
_server.stop()
is_false(_server.is_running(), "stop() 后不再监听")
eq(_server.connected_count(), 0, "stop() 后连接清空")
await _teardown()