Files
backend_v2/internal/vision/client_integration_test.go
toom1996 1de1ec46a6 update
2026-10-18 17:42:10 +08:00

105 lines
3.0 KiB
Go
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.

//go:build integration
// 集成测试:打真实运行的视觉服务,验证 Go 客户端与服务端的接口契约。
//
// 运行前先起服务(本地开发用项目 .venv,走 GPU):
//
// cd D:\project\fashion-siglip
// .venv\Scripts\python.exe -m uvicorn app:app --host 127.0.0.1 --port 8900 --workers 1
//
// 然后:
//
// go test -tags integration ./internal/vision/ -run TestClientAgainstRealService -v
//
// 服务未启动时本测试 **跳过**(不算失败),以免阻塞没有该服务的环境。
// 可用 VISION_ENDPOINT 环境变量指向其它地址。
package vision
import (
"bytes"
"context"
"encoding/json"
"image"
"image/color"
"image/png"
"net/http"
"os"
"testing"
"time"
)
func TestClientAgainstRealService(t *testing.T) {
endpoint := os.Getenv("VISION_ENDPOINT")
if endpoint == "" {
endpoint = "http://127.0.0.1:8900"
}
// 1) 探活:不通就跳过(本测试依赖外部进程,不应让 CI 变红)。
hc := &http.Client{Timeout: 3 * time.Second}
resp, err := hc.Get(endpoint + "/health")
if err != nil {
t.Skipf("视觉服务不可达(%s),跳过集成测试:%v", endpoint, err)
}
defer func() { _ = resp.Body.Close() }()
var health struct {
Ready bool `json:"ready"`
Dim int `json:"dim"`
Device string `json:"device"`
}
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
t.Fatalf("解析 /health 失败: %v", err)
}
if !health.Ready {
t.Skipf("视觉服务未就绪(模型仍在加载或加载失败),跳过:%+v", health)
}
t.Logf("服务就绪:dim=%d device=%s", health.Dim, health.Device)
// 2) 用一张程序生成的 PNG 验证完整契约:base64 请求 → 服务解码 → 768 维归一化向量。
img := generateTestPNG(t)
c := New(endpoint, health.Dim, 60*time.Second, 3)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
vecs, skipped, err := c.Embed(ctx, [][]byte{img})
if err != nil {
t.Fatalf("Embed 失败: %v", err)
}
if len(skipped) != 0 {
t.Fatalf("合法 PNG 不应被跳过,skipped=%v", skipped)
}
if len(vecs) != 1 {
t.Fatalf("应返回 1 个向量,得到 %d", len(vecs))
}
if got := len(vecs[0]); got != health.Dim {
t.Fatalf("向量维度应为 %d,得到 %d", health.Dim, got)
}
// 服务端承诺已 L2 归一化:模长≈1 是「点积即余弦相似度」的前提。
var sumSq float64
for _, f := range vecs[0] {
sumSq += float64(f) * float64(f)
}
norm := sumSq
if norm < 0.99 || norm > 1.01 {
t.Fatalf("向量应已 L2 归一化(模长²≈1),实际 %f", norm)
}
}
// generateTestPNG 生成一张 64x64 的纯色 PNG(不引入外部测试资源)。
func generateTestPNG(t *testing.T) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
for x := 0; x < 64; x++ {
for y := 0; y < 64; y++ {
img.Set(x, y, color.RGBA{R: 200, G: 60, B: 120, A: 255})
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
t.Fatalf("生成测试 PNG 失败: %v", err)
}
return buf.Bytes()
}