safesight-control/internal/service/template_composer_test.go
tian 25cf456047 feat: console one-click auto-config pipeline
管控台一键自动配置:勾选检测功能 + 分配视频源 → 自动生成配置并下发

新增:
- FeatureRegistry: 功能→模板映射与选择算法
- template_composer: 多模板 DAG 浅合并
- AutoConfigService: 完整流水线
- console.html: 视频源分配交互

修改:
- actionConsoleSave: stub → 完整自动配置编排
- pageConsole: 修复功能勾选回显 + 内存指标解析
- main.go: 注入 AutoConfigService
- .gitignore: /managerd 只匹配根目录二进制
2026-05-12 13:26:49 +08:00

204 lines
5.7 KiB
Go

package service
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func loadJSONTemplate(t *testing.T, name string) map[string]any {
t.Helper()
path := filepath.Join("..", "..", "templates", "standard_templates", name+".json")
body, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
var raw map[string]any
if err := json.Unmarshal(body, &raw); err != nil {
t.Fatalf("parse %s: %v", name, err)
}
return raw
}
func TestComposeTemplates_SingleTemplateIdentity(t *testing.T) {
face := loadJSONTemplate(t, "std_face_recognition_stream")
result, err := ComposeTemplates([]map[string]any{face})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result["name"] == nil {
t.Error("result has no name")
}
}
func TestComposeTemplates_FacePlusShoe(t *testing.T) {
face := loadJSONTemplate(t, "std_face_recognition_stream")
shoe := loadJSONTemplate(t, "std_workshoe_detection_stream")
result, err := ComposeTemplates([]map[string]any{face, shoe})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tmpl, _ := result["template"].(map[string]any)
if tmpl == nil {
t.Fatal("result missing template body")
}
nodes, _ := tmpl["nodes"].([]any)
edges, _ := tmpl["edges"].([]any)
t.Logf("composed name: %v", result["name"])
t.Logf("nodes: %d, edges: %d", len(nodes), len(edges))
// Should have more nodes than either input
if len(nodes) < 10 {
t.Errorf("expected at least 10 nodes, got %d", len(nodes))
}
if len(edges) < 10 {
t.Errorf("expected at least 10 edges, got %d", len(edges))
}
// Verify no duplicate node IDs
ids := map[string]bool{}
for _, n := range nodes {
node, _ := n.(map[string]any)
id := stringValue(node["id"])
if id == "" {
t.Error("node with empty id")
continue
}
if ids[id] {
t.Errorf("duplicate node id: %s", id)
}
ids[id] = true
}
// Verify edges refer to existing nodes
for _, e := range edges {
edge, _ := e.([]any)
if len(edge) < 2 {
t.Error("edge too short")
continue
}
from := stringValue(edge[0])
to := stringValue(edge[1])
if !ids[from] {
t.Errorf("edge from unknown node: %s", from)
}
if !ids[to] {
t.Errorf("edge to unknown node: %s", to)
}
}
// Verify it can be serialized
body, err := json.MarshalIndent(result, "", " ")
if err != nil {
t.Errorf("marshal composed template: %v", err)
}
t.Logf("composed JSON length: %d bytes", len(body))
// Verify key node types exist
hasSource := false
hasPreprocess := false
hasPublish := false
for _, n := range nodes {
node, _ := n.(map[string]any)
t := stringValue(node["type"])
switch {
case strings.HasPrefix(t, "input_"):
hasSource = true
case t == "preprocess":
hasPreprocess = true
case t == "publish":
hasPublish = true
}
}
if !hasSource {
t.Error("composed template missing source node")
}
if !hasPreprocess {
t.Error("composed template missing preprocess node")
}
if !hasPublish {
t.Error("composed template missing publish node")
}
}
func TestComposeTemplates_Empty(t *testing.T) {
_, err := ComposeTemplates(nil)
if err == nil {
t.Fatal("expected error for nil input")
}
}
func TestParseTemplateDAG_Invalid(t *testing.T) {
_, err := parseTemplateDAG(map[string]any{})
if err == nil {
t.Fatal("expected error for empty raw")
}
_, err = parseTemplateDAG(map[string]any{
"name": "test",
"template": map[string]any{
"nodes": []any{},
},
})
if err == nil {
t.Fatal("expected error for no nodes")
}
}
func TestClassifyNode(t *testing.T) {
tests := []struct {
node map[string]any
cls templateNodeType
}{
{map[string]any{"type": "input_rtsp", "role": "source"}, nodeSource},
{map[string]any{"type": "input_file"}, nodeSource},
{map[string]any{"type": "preprocess"}, nodePreprocess},
{map[string]any{"type": "osd"}, nodeOSD},
{map[string]any{"type": "publish"}, nodePublish},
{map[string]any{"type": "publish", "role": "sink"}, nodePublish},
{map[string]any{"type": "alarm"}, nodeAlarm},
{map[string]any{"type": "ai_yolo"}, nodeDetection},
{map[string]any{"type": "tracker"}, nodeDetection},
{map[string]any{"type": "logic_gate"}, nodeDetection},
}
for _, tt := range tests {
got := classifyNode(tt.node)
if got != tt.cls {
t.Errorf("classifyNode(%v) = %d, want %d", tt.node, got, tt.cls)
}
}
}
func TestSanitizeIDPrefix(t *testing.T) {
tests := []struct {
in, want string
}{
{"std_face_recognition_stream", "std_face_recognition_stream"},
{"my-template-v2", "my_template_v2"},
{"foo.bar", "foo_bar"},
{"___abc___", "abc"},
}
for _, tt := range tests {
got := sanitizeIDPrefix(tt.in)
if got != tt.want {
t.Errorf("sanitizeIDPrefix(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestUniqueID(t *testing.T) {
used := map[string]bool{"foo": true, "foo_2": true}
if got := uniqueID("bar", used); got != "bar" {
t.Errorf("uniqueID(bar) = %s, want bar", got)
}
if got := uniqueID("foo", used); got != "foo_3" {
t.Errorf("uniqueID(foo) = %s, want foo_3", got)
}
}