From de4dfcf1012224710f2adba6a90791ac3928b3f6 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Thu, 17 Sep 2026 20:39:41 +0300 Subject: [PATCH] tests --- internal/video/pattern_test.go | 47 +++++++++++++++++++ internal/video/runner.go | 5 --- internal/video/runner_test.go | 82 ++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 internal/video/pattern_test.go create mode 100644 internal/video/runner_test.go diff --git a/internal/video/pattern_test.go b/internal/video/pattern_test.go new file mode 100644 index 0000000..9bce8ba --- /dev/null +++ b/internal/video/pattern_test.go @@ -0,0 +1,47 @@ +package video + +import "testing" + +func TestPatterns(t *testing.T) { + got := Patterns() + if len(got) == 0 { + t.Fatal("Patterns returned no video patterns") + } + + seen := make(map[string]struct{}, len(got)) + for i, pattern := range got { + if pattern.Name == "" { + t.Fatalf("pattern %d has an empty name", i) + } + if pattern.Description == "" { + t.Fatalf("pattern %q has an empty description", pattern.Name) + } + if !HasPattern(pattern.Name) { + t.Fatalf("Patterns returned %q, but HasPattern rejected it", pattern.Name) + } + if _, exists := seen[pattern.Name]; exists { + t.Fatalf("duplicate pattern name %q", pattern.Name) + } + seen[pattern.Name] = struct{}{} + if i > 0 && got[i-1].Name >= pattern.Name { + t.Fatalf("patterns are not sorted: %q appears before %q", got[i-1].Name, pattern.Name) + } + } +} + +func TestHasPatternRejectsUnknownName(t *testing.T) { + if HasPattern("not-a-pattern") { + t.Fatal("HasPattern accepted an unknown pattern") + } +} + +func TestPatternRegistryKeysMatchNames(t *testing.T) { + for name, pattern := range patterns { + if pattern.Name != name { + t.Errorf("pattern map key %q does not match pattern name %q", name, pattern.Name) + } + if pattern.kernelPath == "" { + t.Errorf("pattern %q has an empty kernel path", name) + } + } +} diff --git a/internal/video/runner.go b/internal/video/runner.go index c66b637..500d0d7 100644 --- a/internal/video/runner.go +++ b/internal/video/runner.go @@ -101,11 +101,6 @@ func buildTextOverlay(cfg Config) (overlay *generator.TextOverlay, resultErr err if cfg.Overlay.Text == "" { return nil, nil } - if cfg.Overlay.X < 0 || cfg.Overlay.X > int(cfg.Width()) || - cfg.Overlay.Y < 0 || cfg.Overlay.Y > int(cfg.Height()) { - return nil, fmt.Errorf("text overlay position (%d, %d) is outside the %dx%d video frame", - cfg.Overlay.X, cfg.Overlay.Y, cfg.Width(), cfg.Height()) - } face, err := generator.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 48) if err != nil { diff --git a/internal/video/runner_test.go b/internal/video/runner_test.go new file mode 100644 index 0000000..08dbe21 --- /dev/null +++ b/internal/video/runner_test.go @@ -0,0 +1,82 @@ +package video + +import ( + "strings" + "testing" + + "mxl-pattern-generator/internal/flowdef" +) + +const testVideoID = "b3bb5be7-9fe9-4324-a5bb-4c70e1084449" + +func testConfig(t *testing.T, overlay OverlayConfig) Config { + t.Helper() + definition, err := flowdef.NewV210Video( + testVideoID, + 1920, + 1080, + flowdef.Rational{Numerator: 50, Denominator: 1}, + ) + if err != nil { + t.Fatalf("NewV210Video: %v", err) + } + return Config{Definition: definition, Pattern: "ebu75", Overlay: overlay} +} + +func TestBuildTextOverlayDisabled(t *testing.T) { + overlay, err := buildTextOverlay(testConfig(t, OverlayConfig{})) + if err != nil { + t.Fatalf("buildTextOverlay: %v", err) + } + if overlay != nil { + t.Fatal("buildTextOverlay returned an overlay for empty text") + } +} + +func TestBuildTextOverlayPositioning(t *testing.T) { + // Production assets are resolved from the repository root. + t.Chdir("../..") + + tests := []struct { + name string + overlay OverlayConfig + }{ + {name: "explicit", overlay: OverlayConfig{Text: "MXL", X: 120, Y: 48}}, + {name: "preset", overlay: OverlayConfig{Text: "MXL", Position: "cc"}}, + {name: "preset ignores explicit coordinates", overlay: OverlayConfig{Text: "MXL", X: -1, Y: -1, Position: "cc"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + overlay, err := buildTextOverlay(testConfig(t, tc.overlay)) + if err != nil { + t.Fatalf("buildTextOverlay: %v", err) + } + if overlay == nil { + t.Fatal("buildTextOverlay returned nil") + } + }) + } +} + +func TestBuildTextOverlayRejectsInvalidPosition(t *testing.T) { + t.Chdir("../..") + + tests := []struct { + name string + overlay OverlayConfig + wantErrSub string + }{ + {name: "negative", overlay: OverlayConfig{Text: "MXL", X: -6}, wantErrSub: "must not be negative"}, + {name: "unaligned x", overlay: OverlayConfig{Text: "MXL", X: 7}, wantErrSub: "divisible by 6"}, + {name: "right overflow", overlay: OverlayConfig{Text: "MXL", X: 1902}, wantErrSub: "does not fit"}, + {name: "bottom overflow", overlay: OverlayConfig{Text: "MXL", Y: 1070}, wantErrSub: "does not fit"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := buildTextOverlay(testConfig(t, tc.overlay)) + if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) { + t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub) + } + }) + } +}