Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
## 变更内容
### k8s/ingress.yaml
- 新增 /xiaozhi/v1/ 路径规则,将 WebSocket 流量路由到 hw-ws-svc:8888
- Traefik 最长前缀优先,/xiaozhi/v1/ 不影响 / 下的 Django 路由
### hw_service_go/k8s/service.yaml
- Service 类型由 LoadBalancer 改为 ClusterIP
- 移除阿里云 SLB 注解(通过 Traefik Ingress 统一暴露,不再需要独立公网 IP)
### hw_service_go/k8s/deployment.yaml
- 镜像地址改为 ${CI_REGISTRY_IMAGE}/hw-ws-service:latest 占位符
- CI/CD 部署时统一通过 sed 替换为华为云 SWR 实际地址
### hw_service_go/internal/server/server.go
- 新增 GET /xiaozhi/v1/healthz 接口,返回 {"status":"ok","active_connections":N}
- 用于部署后验证服务存活及当前连接数
### .gitea/workflows/deploy.yaml
- 新增 Build and Push HW WebSocket Service 步骤,构建并推送 hw_service_go 镜像
- 部署步骤新增 kubectl apply hw_service_go/k8s/deployment.yaml 和 service.yaml
- 新增 kubectl rollout restart deployment/hw-ws-service
### run.sh
- 本地同时启动 Django + hw_service_go 的开发脚本
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
5.0 KiB
Go
196 lines
5.0 KiB
Go
package handler_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/qy/hw-ws-service/internal/audio"
|
|
"github.com/qy/hw-ws-service/internal/connection"
|
|
"github.com/qy/hw-ws-service/internal/handler"
|
|
)
|
|
|
|
// makeWSPair creates a real WebSocket pair for testing.
|
|
// svrWS is the server side (used by our Connection), cliWS simulates the hardware.
|
|
func makeWSPair(t *testing.T) (svrWS *websocket.Conn, cliWS *websocket.Conn, cleanup func()) {
|
|
t.Helper()
|
|
|
|
ch := make(chan *websocket.Conn, 1)
|
|
done := make(chan struct{})
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
up := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
|
c, err := up.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
t.Logf("upgrade error: %v", err)
|
|
return
|
|
}
|
|
ch <- c
|
|
<-done
|
|
}))
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
|
cli, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
close(done)
|
|
srv.Close()
|
|
t.Fatalf("dial error: %v", err)
|
|
}
|
|
|
|
svr := <-ch
|
|
return svr, cli, func() {
|
|
close(done)
|
|
svr.Close()
|
|
cli.Close()
|
|
srv.Close()
|
|
}
|
|
}
|
|
|
|
// makeFrames creates n fake Opus frames of 4 bytes each.
|
|
func makeFrames(n int) [][]byte {
|
|
frames := make([][]byte, n)
|
|
for i := range frames {
|
|
frames[i] = []byte{byte(i), byte(i >> 8), 0x00, 0xff}
|
|
}
|
|
return frames
|
|
}
|
|
|
|
// TestSendOpusStream_Empty verifies that an empty frame list returns immediately.
|
|
func TestSendOpusStream_Empty(t *testing.T) {
|
|
svrWS, _, cleanup := makeWSPair(t)
|
|
defer cleanup()
|
|
|
|
conn := connection.New(svrWS, "dev1", "cli1")
|
|
abort := make(chan struct{})
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
handler.SendOpusStream(conn, nil, abort)
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("SendOpusStream did not return immediately for empty frames")
|
|
}
|
|
}
|
|
|
|
// TestSendOpusStream_AllFramesSent verifies that all frames reach the client.
|
|
// Uses PreBufferCount+2 frames so both pre-buffer and timed paths are exercised.
|
|
func TestSendOpusStream_AllFramesSent(t *testing.T) {
|
|
svrWS, cliWS, cleanup := makeWSPair(t)
|
|
defer cleanup()
|
|
|
|
conn := connection.New(svrWS, "dev1", "cli1")
|
|
|
|
totalFrames := audio.PreBufferCount + 2 // 3 pre-buffer + 2 timed
|
|
frames := makeFrames(totalFrames)
|
|
|
|
abort := make(chan struct{})
|
|
senderDone := make(chan struct{})
|
|
go func() {
|
|
handler.SendOpusStream(conn, frames, abort)
|
|
close(senderDone)
|
|
}()
|
|
|
|
// Read all frames from the client side (simulates hardware receiving)
|
|
received := 0
|
|
cliWS.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
for received < totalFrames {
|
|
msgType, _, err := cliWS.ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("client read error after %d frames: %v", received, err)
|
|
}
|
|
if msgType == websocket.BinaryMessage {
|
|
received++
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-senderDone:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("SendOpusStream did not finish after all frames were sent")
|
|
}
|
|
|
|
if received != totalFrames {
|
|
t.Errorf("received %d frames, want %d", received, totalFrames)
|
|
}
|
|
}
|
|
|
|
// TestSendOpusStream_Abort verifies that closing abortCh stops streaming early.
|
|
func TestSendOpusStream_Abort(t *testing.T) {
|
|
svrWS, _, cleanup := makeWSPair(t)
|
|
defer cleanup()
|
|
|
|
conn := connection.New(svrWS, "dev1", "cli1")
|
|
|
|
// Many frames so timing control is active (pre-buffer finishes quickly,
|
|
// then the time.After select can receive the abort signal)
|
|
frames := makeFrames(100)
|
|
|
|
abort := make(chan struct{})
|
|
senderDone := make(chan struct{})
|
|
go func() {
|
|
handler.SendOpusStream(conn, frames, abort)
|
|
close(senderDone)
|
|
}()
|
|
|
|
// Close abort after pre-buffer has had time to finish but before timed frames complete
|
|
time.Sleep(20 * time.Millisecond)
|
|
close(abort)
|
|
|
|
select {
|
|
case <-senderDone:
|
|
// SendOpusStream returned early — correct behaviour
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("SendOpusStream did not abort within 2s after closing abortCh")
|
|
}
|
|
}
|
|
|
|
// TestSendOpusStream_PreBufferOnly verifies frames <= PreBufferCount are all sent
|
|
// without entering the timed loop (should finish nearly instantly).
|
|
func TestSendOpusStream_PreBufferOnly(t *testing.T) {
|
|
svrWS, cliWS, cleanup := makeWSPair(t)
|
|
defer cleanup()
|
|
|
|
conn := connection.New(svrWS, "dev1", "cli1")
|
|
|
|
frames := makeFrames(audio.PreBufferCount) // exactly the pre-buffer count
|
|
abort := make(chan struct{})
|
|
|
|
start := time.Now()
|
|
senderDone := make(chan struct{})
|
|
go func() {
|
|
handler.SendOpusStream(conn, frames, abort)
|
|
close(senderDone)
|
|
}()
|
|
|
|
received := 0
|
|
cliWS.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
for received < len(frames) {
|
|
msgType, _, err := cliWS.ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("read error: %v", err)
|
|
}
|
|
if msgType == websocket.BinaryMessage {
|
|
received++
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-senderDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("sender did not finish")
|
|
}
|
|
|
|
elapsed := time.Since(start)
|
|
// Pre-buffer frames should not wait on the timer; allow 200ms for overhead
|
|
if elapsed > 200*time.Millisecond {
|
|
t.Errorf("pre-buffer-only send took too long: %v (want < 200ms)", elapsed)
|
|
}
|
|
}
|