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) } }