105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
//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()
|
||
}
|