246 lines
7.5 KiB
Go
246 lines
7.5 KiB
Go
package service
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"safesight-control/internal/config"
|
|
"safesight-control/internal/storage"
|
|
)
|
|
|
|
// ── GetDevicesUsingTemplate ──
|
|
|
|
func TestGetDevicesUsingTemplate(t *testing.T) {
|
|
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "tuning.db"))
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLite: %v", err)
|
|
}
|
|
defer store.Close()
|
|
repo := storage.NewAssetsRepo(store.DB())
|
|
svc := NewConfigPreviewService(&config.Config{}, repo)
|
|
|
|
// Seed template
|
|
repo.SaveTemplate("workshop_face_recognition_shoe_alarm", "user template",
|
|
`{"name":"workshop_face_recognition_shoe_alarm","template":{"nodes":[{"id":"in","type":"input_rtsp"}],"edges":[]}}`)
|
|
|
|
// Seed profile referencing the template
|
|
repo.SaveProfile("auto_dev1", "workshop_face_recognition_shoe_alarm", "", "",
|
|
`{"name":"auto_dev1","primary_template_name":"workshop_face_recognition_shoe_alarm"}`)
|
|
repo.SaveProfile("auto_dev2", "workshop_face_recognition_shoe_alarm", "", "",
|
|
`{"name":"auto_dev2","primary_template_name":"workshop_face_recognition_shoe_alarm"}`)
|
|
repo.SaveProfile("auto_other", "other_template", "", "",
|
|
`{"name":"auto_other","primary_template_name":"other_template"}`)
|
|
|
|
// Seed device assignments (bodyJSON must be a JSON object, not array)
|
|
repo.SaveDeviceAssignment("dev1", "auto_dev1", "", `{"recognition_units":[]}`)
|
|
repo.SaveDeviceAssignment("dev2", "auto_dev2", "", `{"recognition_units":[]}`)
|
|
repo.SaveDeviceAssignment("dev3", "auto_other", "", `{"recognition_units":[]}`)
|
|
|
|
// Verify setup: list profiles and assignments
|
|
profiles, _ := repo.ListProfiles()
|
|
for _, p := range profiles {
|
|
t.Logf(" profile: name=%s template=%s", p.Name, p.TemplateName)
|
|
}
|
|
assignments, _ := repo.ListDeviceAssignments()
|
|
for _, a := range assignments {
|
|
t.Logf(" assignment: device=%s profile=%s", a.DeviceID, a.ProfileName)
|
|
}
|
|
|
|
devices, err := svc.GetDevicesUsingTemplate("workshop_face_recognition_shoe_alarm")
|
|
if err != nil {
|
|
t.Fatalf("GetDevicesUsingTemplate: %v", err)
|
|
}
|
|
if len(devices) != 2 {
|
|
t.Errorf("expected 2 devices, got %d: %v", len(devices), devices)
|
|
}
|
|
found := map[string]bool{}
|
|
for _, d := range devices {
|
|
found[d] = true
|
|
}
|
|
if !found["dev1"] || !found["dev2"] {
|
|
t.Errorf("expected dev1 and dev2, got %v", devices)
|
|
}
|
|
if found["dev3"] {
|
|
t.Error("dev3 should not be affected (uses other_template)")
|
|
}
|
|
}
|
|
|
|
func TestGetDevicesUsingTemplate_NoMatches(t *testing.T) {
|
|
store, _ := storage.OpenSQLite(filepath.Join(t.TempDir(), "tuning2.db"))
|
|
defer store.Close()
|
|
repo := storage.NewAssetsRepo(store.DB())
|
|
svc := NewConfigPreviewService(&config.Config{}, repo)
|
|
|
|
devices, err := svc.GetDevicesUsingTemplate("nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("GetDevicesUsingTemplate: %v", err)
|
|
}
|
|
if len(devices) != 0 {
|
|
t.Errorf("expected 0 devices, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
// ── Tuning Definitions ──
|
|
|
|
func TestTuningDefinitions_HaveConsistentPaths(t *testing.T) {
|
|
defs := tuningDefinitions["std_workshop_face_recognition_shoe_alarm"]
|
|
if len(defs) == 0 {
|
|
t.Fatal("expected tuning definitions for std_workshop_face_recognition_shoe_alarm")
|
|
}
|
|
|
|
for _, item := range defs {
|
|
if item.Path == "" {
|
|
t.Error("tuning item has empty path")
|
|
}
|
|
if item.Label == "" {
|
|
t.Errorf("tuning item %q has empty label", item.Path)
|
|
}
|
|
if item.Group == "" {
|
|
t.Errorf("tuning item %q has empty group", item.Path)
|
|
}
|
|
if item.Min >= item.Max {
|
|
t.Errorf("tuning item %q: min(%v) >= max(%v)", item.Path, item.Min, item.Max)
|
|
}
|
|
if item.Step <= 0 {
|
|
t.Errorf("tuning item %q: step must be > 0", item.Path)
|
|
}
|
|
// Verify path format: node_id.field...
|
|
nodeID, path := splitTuningPath(item.Path)
|
|
if nodeID == "" || path == "" {
|
|
t.Errorf("tuning item %q: invalid path format, expected node_id.field...", item.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTuningDefinitions_GroupsNotEmpty(t *testing.T) {
|
|
defs := tuningDefinitions["std_workshop_face_recognition_shoe_alarm"]
|
|
groups := map[string]int{}
|
|
for _, item := range defs {
|
|
groups[item.Group]++
|
|
}
|
|
|
|
// Each group should have at least 1 parameter
|
|
for group, count := range groups {
|
|
if count == 0 {
|
|
t.Errorf("group %q is empty", group)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Tuning Path Resolution ──
|
|
|
|
func TestSplitTuningPath(t *testing.T) {
|
|
tests := []struct {
|
|
full, node, rest string
|
|
}{
|
|
{"alarm_violation.rules[0].cooldown_ms", "alarm_violation", "rules[0].cooldown_ms"},
|
|
{"detect_face.conf_thresh", "detect_face", "conf_thresh"},
|
|
{"node_id", "node_id", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
node, rest := splitTuningPath(tt.full)
|
|
if node != tt.node || rest != tt.rest {
|
|
t.Errorf("splitTuningPath(%q) = (%q, %q), want (%q, %q)",
|
|
tt.full, node, rest, tt.node, tt.rest)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveTuningPath(t *testing.T) {
|
|
node := map[string]any{
|
|
"threshold": map[string]any{"accept": 0.45},
|
|
"rules": []any{
|
|
map[string]any{"cooldown_ms": float64(60000), "min_score": 0.5},
|
|
},
|
|
}
|
|
|
|
if v, ok := resolveTuningPath(node, "threshold.accept"); !ok || v != 0.45 {
|
|
t.Errorf("threshold.accept: got %v, want 0.45", v)
|
|
}
|
|
if v, ok := resolveTuningPath(node, "rules[0].cooldown_ms"); !ok || v != 60000 {
|
|
t.Errorf("rules[0].cooldown_ms: got %v, want 60000", v)
|
|
}
|
|
if v, ok := resolveTuningPath(node, "rules[0].min_score"); !ok || v != 0.5 {
|
|
t.Errorf("rules[0].min_score: got %v, want 0.5", v)
|
|
}
|
|
if _, ok := resolveTuningPath(node, "nonexistent.field"); ok {
|
|
t.Error("nonexistent.field should not be found")
|
|
}
|
|
}
|
|
|
|
func TestSetTuningPath(t *testing.T) {
|
|
node := map[string]any{
|
|
"threshold": map[string]any{"accept": 0.45},
|
|
"rules": []any{
|
|
map[string]any{"cooldown_ms": float64(60000)},
|
|
},
|
|
}
|
|
|
|
if err := setTuningPath(node, "threshold.accept", 0.6); err != nil {
|
|
t.Fatalf("setTuningPath: %v", err)
|
|
}
|
|
if v, _ := resolveTuningPath(node, "threshold.accept"); v != 0.6 {
|
|
t.Errorf("after set: got %v, want 0.6", v)
|
|
}
|
|
|
|
if err := setTuningPath(node, "rules[0].cooldown_ms", 30000); err != nil {
|
|
t.Fatalf("setTuningPath: %v", err)
|
|
}
|
|
if v, _ := resolveTuningPath(node, "rules[0].cooldown_ms"); v != 30000 {
|
|
t.Errorf("after set: got %v, want 30000", v)
|
|
}
|
|
}
|
|
|
|
func TestSetTuningPath_InvalidIndex(t *testing.T) {
|
|
node := map[string]any{
|
|
"rules": []any{map[string]any{"x": float64(1)}},
|
|
}
|
|
if err := setTuningPath(node, "rules[5].x", 42); err == nil {
|
|
t.Error("expected error for out-of-range index")
|
|
}
|
|
}
|
|
|
|
// ── Device Overlay Persistence ──
|
|
|
|
func TestSaveAndGetDeviceOverlays(t *testing.T) {
|
|
store, _ := storage.OpenSQLite(filepath.Join(t.TempDir(), "overlay.db"))
|
|
defer store.Close()
|
|
repo := storage.NewAssetsRepo(store.DB())
|
|
svc := NewConfigPreviewService(&config.Config{}, repo)
|
|
|
|
if err := svc.SaveDeviceOverlays("dev1", []string{"face_debug", "production_quiet"}); err != nil {
|
|
t.Fatalf("SaveDeviceOverlays: %v", err)
|
|
}
|
|
|
|
overlays, err := svc.GetDeviceOverlays("dev1")
|
|
if err != nil {
|
|
t.Fatalf("GetDeviceOverlays: %v", err)
|
|
}
|
|
if len(overlays) != 2 {
|
|
t.Errorf("expected 2 overlays, got %d: %v", len(overlays), overlays)
|
|
}
|
|
|
|
// Overwrite
|
|
if err := svc.SaveDeviceOverlays("dev1", []string{}); err != nil {
|
|
t.Fatalf("SaveDeviceOverlays (empty): %v", err)
|
|
}
|
|
overlays, _ = svc.GetDeviceOverlays("dev1")
|
|
if len(overlays) != 0 {
|
|
t.Errorf("expected 0 overlays after clear, got %d", len(overlays))
|
|
}
|
|
}
|
|
|
|
func TestGetDeviceOverlays_UnknownDevice(t *testing.T) {
|
|
store, _ := storage.OpenSQLite(filepath.Join(t.TempDir(), "overlay2.db"))
|
|
defer store.Close()
|
|
svc := NewConfigPreviewService(&config.Config{}, storage.NewAssetsRepo(store.DB()))
|
|
|
|
overlays, err := svc.GetDeviceOverlays("nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("GetDeviceOverlays: %v", err)
|
|
}
|
|
if len(overlays) != 0 {
|
|
t.Errorf("expected empty for unknown device, got %v", overlays)
|
|
}
|
|
}
|