package viewmap import ( "strings" "testing" "github.com/lwoodard/rtsp-streamer/internal/protect" ) // view builds a live view whose slots each hold one camera id. func view(name string, ids ...string) protect.LiveView { v := protect.LiveView{Name: name} for _, id := range ids { v.Slots = append(v.Slots, protect.LiveViewSlot{Cameras: []string{id}}) } return v } // namesFor maps ids "c0".."cN-1" to themselves so tiles are easy to assert on. func namesFor(n int) map[string]string { m := map[string]string{} for i := 0; i < n; i++ { m[id(i)] = id(i) } return m } func id(i int) string { return string(rune('a' + i)) } func ids(n int) []string { out := make([]string, n) for i := range out { out[i] = id(i) } return out } // The 8-camera Protect preset is asymmetric: four 2x2 tiles plus a right-hand // column of four 1x1 tiles, tiling a 5x4 grid exactly. func TestLayoutFromView_EightSlotPreset(t *testing.T) { got, warns := LayoutFromView(view("Office View", ids(8)...), namesFor(8)) if got.Grid != "5x4" { t.Fatalf("grid = %q, want 5x4", got.Grid) } if len(warns) != 0 { t.Errorf("unexpected warnings: %v", warns) } if got.ProtectView != "Office View" || got.Name != "office-view" { t.Errorf("name/link = %q/%q", got.Name, got.ProtectView) } want := []struct{ col, row, cs, rs int }{ {0, 0, 2, 2}, {2, 0, 2, 2}, {4, 0, 1, 1}, {4, 1, 1, 1}, {0, 2, 2, 2}, {2, 2, 2, 2}, {4, 2, 1, 1}, {4, 3, 1, 1}, } if len(got.Tiles) != len(want) { t.Fatalf("got %d tiles, want %d", len(got.Tiles), len(want)) } for i, w := range want { tile := got.Tiles[i] cs, rs := tile.Span() if tile.Col != w.col || tile.Row != w.row || cs != w.cs || rs != w.rs { t.Errorf("tile %d = (%d,%d)+%dx%d, want (%d,%d)+%dx%d", i, tile.Col, tile.Row, cs, rs, w.col, w.row, w.cs, w.rs) } if tile.Camera != id(i) { t.Errorf("tile %d camera = %q, want %q", i, tile.Camera, id(i)) } } } // The 8-slot preset must leave no holes; that was the original bug (8 tiles // dropped into a 3x3 grid left the bottom-right cell blank). func TestLayoutFromView_EightSlotsTileGridExactly(t *testing.T) { got, _ := LayoutFromView(view("Office View", ids(8)...), namesFor(8)) if n := unfilled(presetForSlots(8), got.Tiles); n != 0 { t.Errorf("%d cells left empty, want 0", n) } } // Counts without a preset keep the uniform row-major grid. func TestLayoutFromView_UniformFallback(t *testing.T) { got, warns := LayoutFromView(view("Default", ids(9)...), namesFor(9)) if got.Grid != "3x3" { t.Fatalf("grid = %q, want 3x3", got.Grid) } if len(warns) != 0 { t.Errorf("unexpected warnings: %v", warns) } for i, tile := range got.Tiles { if tile.Col != i%3 || tile.Row != i/3 { t.Errorf("tile %d at (%d,%d), want (%d,%d)", i, tile.Col, tile.Row, i%3, i/3) } } } // A count whose uniform grid is larger than the slot count leaves holes, and // the caller must be told rather than silently shipping a wall with blanks. func TestLayoutFromView_WarnsOnHoles(t *testing.T) { got, warns := LayoutFromView(view("Seven", ids(7)...), namesFor(7)) if got.Grid != "3x3" { t.Fatalf("grid = %q, want 3x3", got.Grid) } if !hasWarn(warns, "empty") { t.Errorf("expected an empty-cell warning, got %v", warns) } } // An unknown camera id is reported and its cell left out. func TestLayoutFromView_WarnsOnUnknownCamera(t *testing.T) { got, warns := LayoutFromView(view("Office View", ids(8)...), namesFor(7)) if len(got.Tiles) != 7 { t.Errorf("got %d tiles, want 7", len(got.Tiles)) } if !hasWarn(warns, "not in config") { t.Errorf("expected an unknown-camera warning, got %v", warns) } } func TestUnfilledCountsUncoveredCells(t *testing.T) { p := uniformPreset(3, 3) if n := unfilled(p, nil); n != 9 { t.Errorf("empty layout: got %d uncovered, want 9", n) } full, _ := LayoutFromView(view("Default", ids(9)...), namesFor(9)) if n := unfilled(p, full.Tiles); n != 0 { t.Errorf("full 3x3: got %d uncovered, want 0", n) } } func hasWarn(warns []string, substr string) bool { for _, w := range warns { if strings.Contains(w, substr) { return true } } return false }