| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292 |
- package tests
- import (
- "bufio"
- "bytes"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "image"
- "image/color"
- "image/draw"
- "image/jpeg"
- "io"
- "net"
- "net/http"
- "net/url"
- "os"
- "strings"
- "testing"
- "time"
- )
- // Qwen3ChatRequest 定义Qwen3聊天请求结构
- type Qwen3ChatRequest struct {
- Model string `json:"model"`
- Messages []ChatMessage `json:"messages"`
- Stream bool `json:"stream"`
- Temperature float64 `json:"temperature,omitempty"`
- }
- // ChatMessage 定义聊天消息结构
- type ChatMessage struct {
- Role string `json:"role"`
- Content string `json:"content"`
- }
- // Qwen3ChatResponse 定义Qwen3聊天响应结构(非流式)
- type Qwen3ChatResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []struct {
- Index int `json:"index"`
- Message struct {
- Role string `json:"role"`
- Content string `json:"content"`
- Refusal *string `json:"refusal"`
- Annotations *string `json:"annotations"`
- Audio *string `json:"audio"`
- FunctionCall *string `json:"function_call"`
- ToolCalls []interface{} `json:"tool_calls"`
- ReasoningContent *string `json:"reasoning_content"`
- } `json:"message"`
- Logprobs *string `json:"logprobs"`
- FinishReason string `json:"finish_reason"`
- StopReason *string `json:"stop_reason"`
- } `json:"choices"`
- ServiceTier *string `json:"service_tier"`
- SystemFingerprint *string `json:"system_fingerprint"`
- Usage struct {
- PromptTokens int `json:"prompt_tokens"`
- TotalTokens int `json:"total_tokens"`
- CompletionTokens int `json:"completion_tokens"`
- PromptTokensDetails *string `json:"prompt_tokens_details"`
- } `json:"usage"`
- PromptLogprobs *string `json:"prompt_logprobs"`
- KvTransferParams *string `json:"kv_transfer_params"`
- }
- // Qwen3StreamResponse 定义Qwen3流式响应结构
- type Qwen3StreamResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- SystemFingerprint string `json:"system_fingerprint,omitempty"`
- Choices []struct {
- Index int `json:"index"`
- Delta struct {
- Role string `json:"role,omitempty"`
- Content string `json:"content,omitempty"`
- ToolCalls []struct {
- Index int `json:"index"`
- ID string `json:"id"`
- Type string `json:"type"`
- Function struct {
- Name string `json:"name"`
- Arguments string `json:"arguments"`
- } `json:"function"`
- } `json:"tool_calls,omitempty"`
- } `json:"delta"`
- Logprobs interface{} `json:"logprobs"`
- FinishReason *string `json:"finish_reason"`
- StopReason *string `json:"stop_reason,omitempty"`
- } `json:"choices"`
- }
- // TestQwen3ChatAPI 测试Qwen3聊天接口
- func TestQwen3ChatAPI(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.Qwen3BaseURL
- model := config.Qwen3Model
- testCases := []struct {
- name string
- message string
- }{
- {
- name: "基础问候测试",
- message: "你好,请介绍一下你自己。",
- },
- {
- name: "技术问题测试",
- message: "请解释一下什么是编程?",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // 构建非流式请求
- request := Qwen3ChatRequest{
- Model: model,
- Stream: false,
- Messages: []ChatMessage{
- {
- Role: "system",
- Content: "你是一个乐于助人的助手。",
- },
- {
- Role: "user",
- Content: tc.message,
- },
- },
- }
- jsonData, err := json.Marshal(request)
- if err != nil {
- t.Fatalf("序列化请求失败: %v", err)
- }
- t.Logf("发送的请求: %s", string(jsonData))
- // 发送POST请求
- resp, err := http.Post(
- fmt.Sprintf("%s/v1/chat/completions", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("发送请求失败: %v", err)
- }
- defer resp.Body.Close()
- t.Logf("响应状态码: %d", resp.StatusCode)
- t.Logf("Content-Type: %s", resp.Header.Get("Content-Type"))
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- t.Fatalf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
- }
- // 读取完整的响应内容
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Fatalf("读取响应失败: %v", err)
- }
- t.Logf("响应长度: %d", len(body))
- // 解析JSON响应
- var response Qwen3ChatResponse
- if err := json.Unmarshal(body, &response); err != nil {
- t.Fatalf("解析JSON响应失败: %v", err)
- }
- // 验证响应
- if response.ID == "" {
- t.Error("响应ID为空")
- }
- if len(response.Choices) == 0 {
- t.Error("响应中没有选择项")
- }
- t.Logf("✅ 非流式响应成功!")
- t.Logf("请求: %s", tc.message)
- t.Logf("响应ID: %s", response.ID)
- t.Logf("模型: %s", response.Model)
- t.Logf("完整响应: %s", response.Choices[0].Message.Content)
- })
- }
- }
- // TestQwen3StreamAPI 测试Qwen3流式聊天接口
- func TestQwen3StreamAPI(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.Qwen3BaseURL
- model := config.Qwen3Model
- testCases := []struct {
- name string
- message string
- }{
- {
- name: "流式基础问候测试",
- message: "你好,请介绍一下你自己。",
- },
- {
- name: "流式技术问题测试",
- message: "请解释一下什么是编程?",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // 构建流式请求
- request := Qwen3ChatRequest{
- Model: model,
- Stream: false,
- Temperature: 0.7,
- Messages: []ChatMessage{
- {
- Role: "system",
- Content: "你是一个乐于助人的助手。",
- },
- {
- Role: "user",
- Content: tc.message,
- },
- },
- }
- jsonData, err := json.Marshal(request)
- if err != nil {
- t.Fatalf("序列化请求失败: %v", err)
- }
- // 发送POST请求
- resp, err := http.Post(
- fmt.Sprintf("%s/v1/chat/completions", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("发送请求失败: %v", err)
- }
- defer resp.Body.Close()
- t.Logf("响应状态码: %d", resp.StatusCode)
- t.Logf("Content-Type: %s", resp.Header.Get("Content-Type"))
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- t.Fatalf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
- }
- t.Logf("响应长度: %d", resp.ContentLength)
- if request.Stream {
- // 处理流式响应
- scanner := bufio.NewScanner(resp.Body)
- var fullContent strings.Builder
- var responseID string
- var firstChunk = true
- t.Logf("开始接收流式响应...")
- chunkCount := 0
- for scanner.Scan() {
- line := scanner.Text()
- chunkCount++
- // 跳过空行和data:前缀
- if line == "" || !strings.HasPrefix(line, "data: ") {
- continue
- }
- // 移除"data: "前缀
- data := strings.TrimPrefix(line, "data: ")
- // 检查是否是结束标记
- if data == "[DONE]" {
- t.Logf("收到结束标记")
- break
- }
- // 解析JSON数据
- var streamResp Qwen3StreamResponse
- if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
- t.Logf("解析流式响应失败: %v, 数据: %s", err, data)
- continue
- }
- // 记录第一个块的ID
- if firstChunk {
- responseID = streamResp.ID
- t.Logf("响应ID: %s", responseID)
- t.Logf("模型: %s", streamResp.Model)
- firstChunk = false
- }
- // 处理choices中的内容
- if len(streamResp.Choices) > 0 {
- choice := streamResp.Choices[0]
- if choice.Delta.Content != "" {
- fullContent.WriteString(choice.Delta.Content)
- t.Logf("收到内容块: %s", choice.Delta.Content)
- }
- // 检查是否完成
- if choice.FinishReason != nil {
- t.Logf("完成原因: %s", *choice.FinishReason)
- break
- }
- }
- }
- if err := scanner.Err(); err != nil {
- t.Fatalf("读取流式响应失败: %v", err)
- }
- // 验证结果
- finalContent := fullContent.String()
- if finalContent == "" {
- t.Error("流式响应内容为空")
- } else {
- t.Logf("✅ 流式响应成功!")
- t.Logf("请求: %s", tc.message)
- t.Logf("响应ID: %s", responseID)
- t.Logf("总块数: %d", chunkCount)
- t.Logf("完整响应: %s", finalContent)
- }
- } else {
- // 处理非流式响应
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Fatalf("读取响应失败: %v", err)
- }
- t.Logf("开始解析非流式响应...")
- // 解析JSON响应
- var response Qwen3ChatResponse
- if err := json.Unmarshal(body, &response); err != nil {
- t.Fatalf("解析JSON响应失败: %v", err)
- }
- // 验证响应
- if response.ID == "" {
- t.Error("响应ID为空")
- }
- if len(response.Choices) == 0 {
- t.Error("响应中没有选择项")
- }
- t.Logf("✅ 非流式响应成功!")
- t.Logf("请求: %s", tc.message)
- t.Logf("响应ID: %s", response.ID)
- t.Logf("模型: %s", response.Model)
- t.Logf("完整响应: %s", response.Choices[0].Message.Content)
- }
- })
- }
- }
- // TestYOLOPredictAPI 测试YOLO图像识别接口
- func TestYOLOPredictAPI(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.YOLOBaseURL
- testCase := struct {
- modeltype string
- image string
- conf_threshold float32
- }{
- modeltype: "gas_station",
- image: "test_images/1.jpg",
- conf_threshold: 0.5,
- }
- // 使用单个测试用例,不需要循环
- tc := testCase
- // 检查图像文件是否存在
- if _, err := os.Stat(tc.image); os.IsNotExist(err) {
- t.Skipf("测试图像文件不存在: %s", tc.image)
- }
- // 读取图像文件并转换为base64
- imageData, err := os.ReadFile(tc.image)
- if err != nil {
- t.Fatalf("读取图像文件失败: %v", err)
- }
- // 解码图像用于后续处理
- img, _, err := image.Decode(bytes.NewReader(imageData))
- if err != nil {
- t.Fatalf("解码图像失败: %v", err)
- }
- // 将图像数据转换为base64
- imageBase64 := base64.StdEncoding.EncodeToString(imageData)
- // 构建JSON请求体
- requestBody := map[string]interface{}{
- "modeltype": tc.modeltype,
- "image": imageBase64,
- "conf_threshold": tc.conf_threshold,
- }
- jsonData, err := json.Marshal(requestBody)
- if err != nil {
- t.Fatalf("序列化请求体失败: %v", err)
- }
- resp, err := http.Post(
- fmt.Sprintf("%s/predict", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("发送请求失败: %v", err)
- }
- defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Fatalf("读取响应失败: %v", err)
- }
- if resp.StatusCode != http.StatusOK {
- t.Errorf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
- }
- if len(body) == 0 {
- t.Error("响应内容为空")
- }
- t.Logf("响应状态码: %d", resp.StatusCode)
- t.Logf("响应内容: %s", string(body))
- // 解析YOLO响应
- var yoloResp YOLOResponse
- if err := json.Unmarshal(body, &yoloResp); err != nil {
- t.Fatalf("解析YOLO响应失败: %v", err)
- }
- t.Logf("解析成功: 检测到 %d 个对象", len(yoloResp.Labels))
- t.Logf("开始绘制边界框...")
- // 在原始图像上绘制边界框
- annotatedImg := drawBoundingBox(img, yoloResp.Boxes, yoloResp.Labels, yoloResp.Scores)
- t.Logf("边界框绘制完成")
- t.Logf("开始保存图像...")
- // 保存标注后的图像
- outputPath := "test_images/annotated_1.jpg"
- outputFile, err := os.Create(outputPath)
- if err != nil {
- t.Fatalf("创建输出文件失败: %v", err)
- }
- defer outputFile.Close()
- if err := jpeg.Encode(outputFile, annotatedImg, &jpeg.Options{Quality: 95}); err != nil {
- t.Fatalf("保存图像失败: %v", err)
- }
- t.Logf("✅ 已保存标注后的图像到: %s", outputPath)
- t.Logf("识别结果: 检测到 %d 个对象", len(yoloResp.Labels))
- for i, label := range yoloResp.Labels {
- if i < len(yoloResp.Scores) {
- t.Logf(" - %s: 置信度 %.2f%%", label, yoloResp.Scores[i]*100)
- }
- }
- // 强制输出日志
- t.Logf("测试完成,图像已保存")
- }
- // TestAPIEndpoints 测试API端点连通性
- func TestAPIEndpoints(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- endpoints := []struct {
- name string
- url string
- }{
- {"Qwen3聊天接口", config.GetQwen3ChatURL()},
- {"YOLO预测接口", config.GetYOLOPredictURL()},
- {"TTS流式接口", config.GetTTSStreamURL()},
- }
- for _, endpoint := range endpoints {
- t.Run(endpoint.name, func(t *testing.T) {
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
- resp, err := client.Head(endpoint.url)
- if err != nil {
- t.Logf("端点 %s 不可达: %v", endpoint.name, err)
- return
- }
- defer resp.Body.Close()
- t.Logf("端点 %s 可达,状态码: %d", endpoint.name, resp.StatusCode)
- })
- }
- }
- // BenchmarkQwen3ChatAPI 性能测试Qwen3聊天接口
- func BenchmarkQwen3ChatAPI(b *testing.B) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.Qwen3BaseURL
- model := config.Qwen3Model
- request := Qwen3ChatRequest{
- Model: model,
- Messages: []ChatMessage{
- {
- Role: "user",
- Content: "你好",
- },
- },
- }
- jsonData, _ := json.Marshal(request)
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- resp, err := http.Post(
- fmt.Sprintf("%s/v1/chat/completions", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- b.Fatalf("请求失败: %v", err)
- }
- resp.Body.Close()
- }
- }
- // YOLOResponse 定义YOLO响应结构
- type YOLOResponse struct {
- Boxes [][]float64 `json:"boxes"`
- Labels []string `json:"labels"`
- Scores []float64 `json:"scores"`
- ModelType string `json:"model_type"`
- }
- // drawBoundingBox 在图像上绘制边界框和标签
- func drawBoundingBox(img image.Image, boxes [][]float64, labels []string, scores []float64) image.Image {
- // 创建可绘制的图像副本
- bounds := img.Bounds()
- drawableImg := image.NewRGBA(bounds)
- draw.Draw(drawableImg, bounds, img, image.Point{}, draw.Src)
- // 红色边界框
- red := image.NewUniform(color.RGBA{255, 0, 0, 255})
- fmt.Printf("图像边界: %v\n", bounds)
- for i, box := range boxes {
- if len(box) >= 4 {
- x1, y1, x2, y2 := int(box[0]), int(box[1]), int(box[2]), int(box[3])
- fmt.Printf("边界框 %d: [%d, %d, %d, %d]\n", i, x1, y1, x2, y2)
- // 绘制边界框
- drawRect(drawableImg, x1, y1, x2, y2, red)
- // 绘制标签和置信度(暂时注释掉,避免白色区域)
- // if i < len(labels) && i < len(scores) {
- // label := fmt.Sprintf("%s: %.2f", labels[i], scores[i])
- // drawLabel(drawableImg, x1, y1-20, label)
- // }
- }
- }
- return drawableImg
- }
- // drawRect 绘制矩形
- func drawRect(img *image.RGBA, x1, y1, x2, y2 int, color *image.Uniform) {
- bounds := img.Bounds()
- fmt.Printf("绘制矩形: [%d, %d, %d, %d], 图像边界: %v\n", x1, y1, x2, y2, bounds)
- // 确保坐标在图像边界内
- if x1 < bounds.Min.X {
- x1 = bounds.Min.X
- }
- if y1 < bounds.Min.Y {
- y1 = bounds.Min.Y
- }
- if x2 >= bounds.Max.X {
- x2 = bounds.Max.X - 1
- }
- if y2 >= bounds.Max.Y {
- y2 = bounds.Max.Y - 1
- }
- fmt.Printf("调整后坐标: [%d, %d, %d, %d]\n", x1, y1, x2, y2)
- // 绘制四条边
- for x := x1; x <= x2; x++ {
- img.Set(x, y1, color)
- img.Set(x, y2, color)
- }
- for y := y1; y <= y2; y++ {
- img.Set(x1, y, color)
- img.Set(x2, y, color)
- }
- fmt.Printf("矩形绘制完成\n")
- }
- // drawLabel 绘制标签文本(简化版本,只绘制背景矩形)
- func drawLabel(img *image.RGBA, x, y int, text string) {
- fmt.Printf("绘制标签: '%s' 在位置 [%d, %d]\n", text, x, y)
- // 绘制白色背景矩形
- white := image.NewUniform(color.RGBA{255, 255, 255, 255})
- labelWidth := len(text) * 8 // 估算文本宽度
- labelHeight := 16
- // 确保标签在图像边界内
- bounds := img.Bounds()
- if x < bounds.Min.X {
- x = bounds.Min.X
- }
- if y < bounds.Min.Y {
- y = bounds.Min.Y
- }
- if x+labelWidth >= bounds.Max.X {
- labelWidth = bounds.Max.X - x - 1
- }
- if y+labelHeight >= bounds.Max.Y {
- labelHeight = bounds.Max.Y - y - 1
- }
- fmt.Printf("标签尺寸: %dx%d\n", labelWidth, labelHeight)
- // 绘制背景矩形
- for dx := 0; dx < labelWidth; dx++ {
- for dy := 0; dy < labelHeight; dy++ {
- if x+dx < bounds.Max.X && y+dy < bounds.Max.Y && x+dx >= bounds.Min.X && y+dy >= bounds.Min.Y {
- img.Set(x+dx, y+dy, white)
- }
- }
- }
- fmt.Printf("标签绘制完成\n")
- }
- // TestHealthCheck 测试健康检查接口
- func TestHealthCheck(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.HealthBaseURL
- t.Run("健康检查测试", func(t *testing.T) {
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
- // 发送POST请求到健康检查端点
- t.Logf("请求URL: %s/health", baseURL)
- resp, err := client.Post(fmt.Sprintf("%s/health", baseURL), "application/json", nil)
- if err != nil {
- t.Logf("❌ 健康检查请求失败: %v", err)
- t.Logf("错误类型: %T", err)
- // 检查是否是网络相关错误
- if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
- t.Logf("⚠️ 这是超时错误")
- }
- // 尝试更详细的错误信息
- if urlErr, ok := err.(*url.Error); ok {
- t.Logf("URL错误详情: %+v", urlErr)
- if urlErr.Err != nil {
- t.Logf("底层错误: %v", urlErr.Err)
- }
- }
- t.Fatalf("健康检查请求失败: %v", err)
- }
- defer resp.Body.Close()
- // 读取响应内容
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Logf("读取响应失败: %v", err)
- }
- t.Logf("健康检查响应状态码: %d", resp.StatusCode)
- t.Logf("响应头: %+v", resp.Header)
- t.Logf("响应内容: %s", string(body))
- // 验证响应状态码
- if resp.StatusCode != http.StatusOK {
- t.Errorf("健康检查失败,期望状态码200,实际状态码: %d", resp.StatusCode)
- }
- // 验证响应内容(通常健康检查返回简单的状态信息)
- if len(body) == 0 {
- t.Error("健康检查响应内容为空")
- }
- // 尝试解析JSON响应(如果返回的是JSON格式)
- var healthResponse map[string]interface{}
- if err := json.Unmarshal(body, &healthResponse); err == nil {
- t.Logf("✅ 健康检查返回JSON格式响应: %+v", healthResponse)
- } else {
- t.Logf("健康检查返回非JSON格式响应: %s", string(body))
- }
- })
- }
- // SearchRequest 定义搜索请求结构
- type SearchRequest struct {
- Query string `json:"query"`
- NResults int `json:"n_results"`
- }
- // SearchResponse 定义搜索响应结构
- type SearchResponse struct {
- Results []SearchResult `json:"results"`
- Total int `json:"total"`
- Query string `json:"query"`
- }
- // SearchResult 定义搜索结果结构
- type SearchResult struct {
- ID string `json:"id"`
- Title string `json:"title"`
- Content string `json:"content"`
- Score float64 `json:"score"`
- Metadata map[string]interface{} `json:"metadata,omitempty"`
- }
- // TestSearchAPI 测试搜索功能接口
- func TestSearchAPI(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.SearchBaseURL
- testCases := []struct {
- name string
- query string
- nResults int
- }{
- {
- name: "技术搜索测试",
- query: "技术",
- nResults: 3,
- },
- {
- name: "编程搜索测试",
- query: "编程",
- nResults: 5,
- },
- {
- name: "AI搜索测试",
- query: "人工智能",
- nResults: 2,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // 构建搜索请求
- request := SearchRequest{
- Query: tc.query,
- NResults: tc.nResults,
- }
- jsonData, err := json.Marshal(request)
- if err != nil {
- t.Fatalf("序列化搜索请求失败: %v", err)
- }
- // 发送POST请求到搜索端点
- resp, err := http.Post(
- fmt.Sprintf("%s/search", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("搜索请求失败: %v", err)
- }
- defer resp.Body.Close()
- // 读取响应内容
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Fatalf("读取搜索响应失败: %v", err)
- }
- t.Logf("搜索响应状态码: %d", resp.StatusCode)
- t.Logf("搜索查询: %s", tc.query)
- t.Logf("请求结果数量: %d", tc.nResults)
- // 验证响应状态码
- if resp.StatusCode != http.StatusOK {
- t.Errorf("搜索请求失败,期望状态码200,实际状态码: %d", resp.StatusCode)
- if len(body) < 500 {
- t.Logf("错误响应内容: %s", string(body))
- }
- return
- }
- // 验证响应内容不为空
- if len(body) == 0 {
- t.Error("搜索响应内容为空")
- return
- }
- // 尝试解析JSON响应
- var searchResponse SearchResponse
- if err := json.Unmarshal(body, &searchResponse); err != nil {
- t.Logf("⚠️ 搜索响应不是标准JSON格式: %v", err)
- t.Logf("响应内容: %s", string(body))
- // 尝试解析为简单的map结构
- var simpleResponse map[string]interface{}
- if err := json.Unmarshal(body, &simpleResponse); err == nil {
- t.Logf("✅ 搜索返回简单JSON格式: %+v", simpleResponse)
- } else {
- t.Logf("搜索返回非JSON格式响应")
- }
- return
- }
- // 验证搜索结果
- t.Logf("✅ 搜索成功!")
- t.Logf("查询: %s", searchResponse.Query)
- t.Logf("总结果数: %d", searchResponse.Total)
- t.Logf("返回结果数: %d", len(searchResponse.Results))
- // 验证结果数量
- if len(searchResponse.Results) > tc.nResults {
- t.Logf("⚠️ 返回结果数量(%d)超过请求数量(%d)", len(searchResponse.Results), tc.nResults)
- }
- // 显示搜索结果详情
- for i, result := range searchResponse.Results {
- t.Logf("结果 %d:", i+1)
- t.Logf(" ID: %s", result.ID)
- t.Logf(" 标题: %s", result.Title)
- t.Logf(" 内容: %s", truncateString(result.Content, 100))
- t.Logf(" 评分: %.4f", result.Score)
- if len(result.Metadata) > 0 {
- t.Logf(" 元数据: %+v", result.Metadata)
- }
- }
- })
- }
- }
- // truncateString 截断字符串到指定长度
- func truncateString(s string, maxLen int) string {
- if len(s) <= maxLen {
- return s
- }
- return s[:maxLen] + "..."
- }
- // TTSRequest 定义TTS请求结构
- type TTSRequest struct {
- Text string `json:"text"`
- }
- // TestTTSStreamAPI 测试TTS流式接口
- func TestTTSStreamAPI(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.TTSBaseURL
- testCases := []struct {
- name string
- text string
- }{
- {
- name: "短文本测试",
- text: "你好,这是一个语音合成测试。",
- },
- {
- name: "中等长度文本测试",
- text: "答案依据:《汽车加油站雷电防护装置检测报告综述表》委托单位随检人员在雷电防护装置检测过程中承担着关键的协同与监督职责。",
- },
- {
- name: "长文本测试",
- text: "答案依据:《汽车加油站雷电防护装置检测报告综述表》委托单位随检人员在雷电防护装置检测过程中承担着关键的协同与监督职责,其参与过程对检测报告的完整性与合规性具有直接影响。作为检测工作的现场对接方和责任主体之一,委托单位随检人员需全程参与检测实施,确保检测工作在真实、可控的现场环境下开展,有效保障检测数据的真实性与代表性。",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // 构建TTS请求
- request := TTSRequest{
- Text: tc.text,
- }
- jsonData, err := json.Marshal(request)
- if err != nil {
- t.Fatalf("序列化TTS请求失败: %v", err)
- }
- t.Logf("发送的TTS请求: %s", string(jsonData))
- t.Logf("请求URL: %s/tts/voice", baseURL)
- // 创建HTTP客户端,设置较长的超时时间(TTS可能需要较长时间)
- client := &http.Client{
- Timeout: 60 * time.Second,
- }
- // 发送POST请求
- resp, err := client.Post(
- fmt.Sprintf("%s/tts/voice", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("发送TTS请求失败: %v", err)
- }
- defer resp.Body.Close()
- t.Logf("TTS响应状态码: %d", resp.StatusCode)
- t.Logf("Content-Type: %s", resp.Header.Get("Content-Type"))
- t.Logf("Content-Length: %s", resp.Header.Get("Content-Length"))
- // 验证响应状态码
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- t.Fatalf("TTS请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
- }
- // 检查Content-Type是否为音频格式
- contentType := resp.Header.Get("Content-Type")
- if contentType != "" {
- t.Logf("响应Content-Type: %s", contentType)
- // WAV格式通常为 "audio/wav" 或 "audio/wave"
- if !strings.Contains(contentType, "audio") && !strings.Contains(contentType, "wav") {
- t.Logf("⚠️ 警告: Content-Type不是预期的音频格式")
- }
- }
- // 读取流式音频数据
- t.Logf("开始接收流式音频数据...")
- var audioData bytes.Buffer
- totalBytes := 0
- chunkCount := 0
- // 分块读取数据
- buffer := make([]byte, 4096) // 4KB缓冲区
- for {
- n, err := resp.Body.Read(buffer)
- if n > 0 {
- audioData.Write(buffer[:n])
- totalBytes += n
- chunkCount++
- // 每读取10个块输出一次进度
- if chunkCount%10 == 0 {
- t.Logf("已接收 %d 字节音频数据 (块数: %d)", totalBytes, chunkCount)
- }
- }
- if err == io.EOF {
- t.Logf("音频数据接收完成")
- break
- }
- if err != nil {
- t.Fatalf("读取音频数据失败: %v", err)
- }
- }
- t.Logf("✅ TTS流式响应成功!")
- t.Logf("请求文本: %s", tc.text)
- t.Logf("文本长度: %d 字符", len(tc.text))
- t.Logf("音频数据总大小: %d 字节", totalBytes)
- t.Logf("接收块数: %d", chunkCount)
- // 验证音频数据不为空
- if totalBytes == 0 {
- t.Error("音频数据为空")
- return
- }
- // 验证音频数据大小合理性(至少应该有WAV文件头)
- if totalBytes < 44 {
- t.Errorf("音频数据太小,可能不是有效的WAV文件: %d 字节", totalBytes)
- }
- // 验证WAV文件头(RIFF格式)
- audioBytes := audioData.Bytes()
- if len(audioBytes) >= 12 {
- // 检查RIFF头
- if string(audioBytes[0:4]) == "RIFF" {
- t.Logf("✅ 检测到有效的RIFF格式音频文件")
- if len(audioBytes) >= 8 {
- fileSize := int(audioBytes[4]) | int(audioBytes[5])<<8 | int(audioBytes[6])<<16 | int(audioBytes[7])<<24
- t.Logf("WAV文件大小字段: %d 字节", fileSize)
- }
- if len(audioBytes) >= 12 && string(audioBytes[8:12]) == "WAVE" {
- t.Logf("✅ 检测到WAVE格式")
- }
- } else {
- t.Logf("⚠️ 警告: 音频数据不是标准的RIFF格式")
- t.Logf("文件头: %x", audioBytes[:min(16, len(audioBytes))])
- }
- }
- // 保存音频文件到本地进行验证
- outputFilename := fmt.Sprintf("tts_test_%s.wav", strings.ReplaceAll(tc.name, " ", "_"))
- outputFile, err := os.Create(outputFilename)
- if err != nil {
- t.Fatalf("创建音频输出文件失败: %v", err)
- }
- defer outputFile.Close()
- _, err = outputFile.Write(audioBytes)
- if err != nil {
- t.Fatalf("写入音频文件失败: %v", err)
- }
- t.Logf("✅ 音频文件已保存: %s", outputFilename)
- t.Logf("文件大小: %d 字节", totalBytes)
- // 验证文件是否成功创建
- if stat, err := os.Stat(outputFilename); err == nil {
- t.Logf("✅ 文件验证成功,实际大小: %d 字节", stat.Size())
- if stat.Size() != int64(totalBytes) {
- t.Errorf("文件大小不匹配: 期望 %d, 实际 %d", totalBytes, stat.Size())
- }
- } else {
- t.Errorf("文件验证失败: %v", err)
- }
- })
- }
- }
- // min 返回两个整数中的较小值
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
- // TestTTSConnectivity 测试TTS接口连通性
- func TestTTSConnectivity(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.TTSBaseURL
- t.Run("TTS接口连通性测试", func(t *testing.T) {
- client := &http.Client{
- Timeout: 30 * time.Second,
- }
- t.Logf("测试TTS接口连通性: %s/tts/voice", baseURL)
- // 构建简单的测试请求
- request := TTSRequest{
- Text: "测试",
- }
- jsonData, err := json.Marshal(request)
- if err != nil {
- t.Fatalf("序列化测试请求失败: %v", err)
- }
- // 发送POST请求
- resp, err := client.Post(
- fmt.Sprintf("%s/tts/voice", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Logf("❌ TTS接口连接失败: %v", err)
- t.Logf("可能的原因:")
- t.Logf(" - TTS服务未启动")
- t.Logf(" - 网络不可达")
- t.Logf(" - 端口不正确")
- t.Logf(" - 防火墙阻止")
- t.Fatalf("TTS接口连通性测试失败: %v", err)
- }
- defer resp.Body.Close()
- t.Logf("✅ TTS接口HTTP连接成功")
- t.Logf("响应状态码: %d", resp.StatusCode)
- t.Logf("Content-Type: %s", resp.Header.Get("Content-Type"))
- // 验证响应状态码
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- t.Logf("⚠️ TTS服务不可用,状态码: %d", resp.StatusCode)
- t.Logf("响应内容: %s", string(body))
- // 根据状态码提供具体的错误信息
- switch resp.StatusCode {
- case 502:
- t.Logf("❌ 502 Bad Gateway - TTS后端服务可能未启动或配置错误")
- case 503:
- t.Logf("❌ 503 Service Unavailable - TTS服务暂时不可用")
- case 404:
- t.Logf("❌ 404 Not Found - TTS接口路径不存在")
- case 500:
- t.Logf("❌ 500 Internal Server Error - TTS服务内部错误")
- default:
- t.Logf("❌ 未知错误状态码: %d", resp.StatusCode)
- }
- } else {
- t.Logf("✅ TTS服务响应正常")
- // 读取少量数据验证是否为音频格式
- buffer := make([]byte, 1024)
- n, err := resp.Body.Read(buffer)
- if err != nil && err != io.EOF {
- t.Logf("读取响应数据失败: %v", err)
- } else if n > 0 {
- t.Logf("✅ 成功接收 %d 字节数据", n)
- // 检查是否为WAV格式
- if n >= 12 {
- if string(buffer[0:4]) == "RIFF" {
- t.Logf("✅ 检测到WAV格式音频数据")
- } else {
- t.Logf("⚠️ 数据格式可能不是WAV: %x", buffer[:min(16, n)])
- }
- }
- }
- }
- })
- }
- // TestHealthAndSearchIntegration 集成测试:先检查健康状态,再进行搜索
- func TestHealthAndSearchIntegration(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.HealthBaseURL
- t.Run("健康检查和搜索集成测试", func(t *testing.T) {
- client := &http.Client{
- Timeout: 15 * time.Second,
- }
- // 第一步:健康检查
- t.Logf("步骤1: 执行健康检查...")
- t.Logf("请求URL: %s/health", baseURL)
- healthResp, err := client.Post(fmt.Sprintf("%s/health", baseURL), "application/json", nil)
- if err != nil {
- t.Logf("❌ 健康检查请求失败: %v", err)
- t.Logf("错误类型: %T", err)
- // 检查是否是网络相关错误
- if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
- t.Logf("⚠️ 这是超时错误")
- }
- // 尝试更详细的错误信息
- if urlErr, ok := err.(*url.Error); ok {
- t.Logf("URL错误详情: %+v", urlErr)
- if urlErr.Err != nil {
- t.Logf("底层错误: %v", urlErr.Err)
- }
- }
- t.Fatalf("健康检查失败: %v", err)
- }
- defer healthResp.Body.Close()
- // 读取响应内容
- healthBody, err := io.ReadAll(healthResp.Body)
- if err != nil {
- t.Logf("读取健康检查响应失败: %v", err)
- }
- t.Logf("健康检查响应状态码: %d", healthResp.StatusCode)
- t.Logf("响应头: %+v", healthResp.Header)
- if len(healthBody) > 0 {
- t.Logf("响应内容: %s", string(healthBody))
- }
- if healthResp.StatusCode != http.StatusOK {
- t.Fatalf("健康检查失败,状态码: %d", healthResp.StatusCode)
- }
- t.Logf("✅ 健康检查通过")
- // 第二步:执行搜索
- t.Logf("步骤2: 执行搜索测试...")
- searchRequest := SearchRequest{
- Query: "技术",
- NResults: 3,
- }
- jsonData, err := json.Marshal(searchRequest)
- if err != nil {
- t.Fatalf("序列化搜索请求失败: %v", err)
- }
- searchResp, err := client.Post(
- fmt.Sprintf("%s/search", baseURL),
- "application/json",
- bytes.NewBuffer(jsonData),
- )
- if err != nil {
- t.Fatalf("搜索请求失败: %v", err)
- }
- defer searchResp.Body.Close()
- body, err := io.ReadAll(searchResp.Body)
- if err != nil {
- t.Fatalf("读取搜索响应失败: %v", err)
- }
- if searchResp.StatusCode != http.StatusOK {
- t.Errorf("搜索失败,状态码: %d", searchResp.StatusCode)
- if len(body) < 200 {
- t.Logf("错误响应: %s", string(body))
- }
- } else {
- t.Logf("✅ 搜索功能正常")
- t.Logf("响应长度: %d 字节", len(body))
- }
- t.Logf("✅ 集成测试完成")
- })
- }
- // TestNetworkConnectivity 测试网络连通性
- func TestNetworkConnectivity(t *testing.T) {
- // 加载测试配置
- config := LoadConfig()
- baseURL := config.HealthBaseURL
- t.Run("网络连通性测试", func(t *testing.T) {
- // 解析URL
- parsedURL, err := url.Parse(baseURL)
- if err != nil {
- t.Fatalf("解析URL失败: %v", err)
- }
- t.Logf("测试目标: %s", baseURL)
- t.Logf("协议: %s", parsedURL.Scheme)
- t.Logf("主机: %s", parsedURL.Host)
- t.Logf("端口: %s", parsedURL.Port())
- // 测试TCP连接
- conn, err := net.DialTimeout("tcp", parsedURL.Host, 5*time.Second)
- if err != nil {
- t.Logf("❌ TCP连接失败: %v", err)
- t.Logf("可能的原因:")
- t.Logf(" - 服务器未启动")
- t.Logf(" - 防火墙阻止")
- t.Logf(" - 网络不可达")
- t.Logf(" - 端口不正确")
- } else {
- t.Logf("✅ TCP连接成功")
- conn.Close()
- }
- // 测试HTTP连接
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
- // 尝试简单的HEAD请求
- resp, err := client.Head(baseURL)
- if err != nil {
- t.Logf("❌ HTTP HEAD请求失败: %v", err)
- } else {
- t.Logf("✅ HTTP连接成功,状态码: %d", resp.StatusCode)
- resp.Body.Close()
- }
- // 尝试POST请求到根路径
- resp, err = client.Post(baseURL, "application/json", nil)
- if err != nil {
- t.Logf("❌ HTTP POST请求失败: %v", err)
- } else {
- t.Logf("✅ HTTP POST连接成功,状态码: %d", resp.StatusCode)
- resp.Body.Close()
- }
- })
- }
|