管控台一键自动配置:勾选检测功能 + 分配视频源 → 自动生成配置并下发 新增: - FeatureRegistry: 功能→模板映射与选择算法 - template_composer: 多模板 DAG 浅合并 - AutoConfigService: 完整流水线 - console.html: 视频源分配交互 修改: - actionConsoleSave: stub → 完整自动配置编排 - pageConsole: 修复功能勾选回显 + 内存指标解析 - main.go: 注入 AutoConfigService - .gitignore: /managerd 只匹配根目录二进制
204 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|