353 lines
8.6 KiB
Go
353 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadPlaylist(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
wantLen int
|
|
wantErr bool
|
|
first string
|
|
}{
|
|
{
|
|
name: "simple paths",
|
|
content: "/videos/video1.mp4\n/videos/video2.mp4\n",
|
|
wantLen: 2,
|
|
first: "/videos/video1.mp4",
|
|
},
|
|
{
|
|
name: "with file prefix",
|
|
content: "file /videos/video1.mp4\nfile '/videos/video2.mp4'\n",
|
|
wantLen: 2,
|
|
first: "/videos/video1.mp4",
|
|
},
|
|
{
|
|
name: "with file prefix double quotes",
|
|
content: "file \"/videos/video1.mp4\"\n",
|
|
wantLen: 1,
|
|
first: "/videos/video1.mp4",
|
|
},
|
|
{
|
|
name: "with comments and blanks",
|
|
content: "# comment\n\n/videos/video1.mp4\n\n# another\n/videos/video2.mp4\n",
|
|
wantLen: 2,
|
|
first: "/videos/video1.mp4",
|
|
},
|
|
{
|
|
name: "empty file",
|
|
content: "",
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "only comments",
|
|
content: "# comment\n# another\n",
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := filepath.Join(tmpDir, tt.name+".txt")
|
|
os.WriteFile(path, []byte(tt.content), 0644)
|
|
|
|
videos, err := LoadPlaylist(path)
|
|
if tt.wantErr && err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
if !tt.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if len(videos) != tt.wantLen {
|
|
t.Errorf("len = %d, want %d", len(videos), tt.wantLen)
|
|
}
|
|
if tt.wantLen > 0 && videos[0].Path != tt.first {
|
|
t.Errorf("videos[0].Path = %q, want %q", videos[0].Path, tt.first)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadPlaylist_FileNotFound(t *testing.T) {
|
|
_, err := LoadPlaylist("/nonexistent/playlist.txt")
|
|
if err == nil {
|
|
t.Error("expected error for missing file, got nil")
|
|
}
|
|
}
|
|
|
|
func TestValidateKeyframeInterval(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
csvData string
|
|
wantErr bool
|
|
errText string
|
|
}{
|
|
{
|
|
name: "valid keyframes (gaps <=2s)",
|
|
csvData: "1,0.00\n0,0.50\n1,1.00\n0,1.50\n1,2.00\n0,2.50\n1,3.00\n",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "gap too large >2s",
|
|
csvData: "1,0.00\n0,1.00\n1,3.50\n0,4.00\n1,6.00\n",
|
|
wantErr: true,
|
|
errText: "keyframe interval too large",
|
|
},
|
|
{
|
|
name: "insufficient keyframes (<2)",
|
|
csvData: "1,0.00\n",
|
|
wantErr: true,
|
|
errText: "insufficient keyframes",
|
|
},
|
|
{
|
|
name: "empty output",
|
|
csvData: "",
|
|
wantErr: true,
|
|
errText: "keyframe probe failed",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// create temp ffprobe that outputs csvData
|
|
tmpDir := t.TempDir()
|
|
mockProbe := filepath.Join(tmpDir, "ffprobe")
|
|
var script string
|
|
if tt.csvData == "" {
|
|
script = "@echo off\necho\nexit /b 1\n"
|
|
} else {
|
|
script = "@echo off\necho " + strings.ReplaceAll(tt.csvData, "\n", "& echo ") + "\nexit /b 0\n"
|
|
}
|
|
os.WriteFile(mockProbe+".bat", []byte(script), 0755)
|
|
os.Setenv("PATH", tmpDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
defer os.Unsetenv("PATH")
|
|
|
|
err := validateKeyframeInterval("test.mp4")
|
|
if tt.wantErr && err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
if !tt.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if tt.wantErr && tt.errText != "" && err != nil {
|
|
if !strings.Contains(err.Error(), tt.errText) {
|
|
t.Errorf("error = %v, want containing %q", err, tt.errText)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateVideoStrict(t *testing.T) {
|
|
// ValidateVideoStrict calls ValidateVideo which calls ffprobe
|
|
// Test only the result structure when ValidateVideo returns valid=false
|
|
// Mock not feasible without full ffprobe mock, skip for now
|
|
t.Skip("requires ffprobe mock or test media fixtures")
|
|
}
|
|
|
|
func TestHealthHandler(t *testing.T) {
|
|
// Save and restore globals
|
|
prevMode := playoutMode
|
|
prevCount := restartCount
|
|
playoutMode = "live"
|
|
restartCount = 3
|
|
defer func() {
|
|
playoutMode = prevMode
|
|
restartCount = prevCount
|
|
}()
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
HealthHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, `"healthy"`) {
|
|
t.Errorf("body missing healthy: %s", body)
|
|
}
|
|
if !strings.Contains(body, `"playout"`) {
|
|
t.Errorf("body missing service: %s", body)
|
|
}
|
|
if !strings.Contains(body, `"restarts":3`) {
|
|
t.Errorf("body missing restarts: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHealthHandler_Degraded(t *testing.T) {
|
|
prevMode := playoutMode
|
|
playoutMode = "slate"
|
|
defer func() { playoutMode = prevMode }()
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
HealthHandler(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, `"degraded"`) {
|
|
t.Errorf("body missing degraded: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestShouldEscalateToSlate(t *testing.T) {
|
|
// Save globals
|
|
prevCount := restartCount
|
|
prevWindow := restartWindow
|
|
defer func() {
|
|
restartCount = prevCount
|
|
restartWindow = prevWindow
|
|
}()
|
|
|
|
// Under threshold
|
|
restartWindow = time.Now()
|
|
restartCount = 3
|
|
if shouldEscalateToSlate() {
|
|
t.Error("shouldEscalateToSlate() = true, want false (under threshold)")
|
|
}
|
|
|
|
// At threshold
|
|
restartCount = maxRestartsPerMinute
|
|
if !shouldEscalateToSlate() {
|
|
t.Error("shouldEscalateToSlate() = false, want true (at threshold)")
|
|
}
|
|
|
|
// Over threshold
|
|
restartCount = 7
|
|
if !shouldEscalateToSlate() {
|
|
t.Error("shouldEscalateToSlate() = false, want true (over threshold)")
|
|
}
|
|
|
|
// Window expired
|
|
restartWindow = time.Now().Add(-61 * time.Second)
|
|
restartCount = 10
|
|
if shouldEscalateToSlate() {
|
|
t.Error("shouldEscalateToSlate() = true, want false (window expired, should reset)")
|
|
}
|
|
if restartCount != 0 {
|
|
t.Errorf("restartCount after window reset = %d, want 0", restartCount)
|
|
}
|
|
}
|
|
|
|
func TestResetHLSOutput(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create some segment files
|
|
os.MkdirAll(tmpDir, 0755)
|
|
os.WriteFile(filepath.Join(tmpDir, "segment_test.ts"), []byte("data"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "segment_test.m4s"), []byte("data"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "playlist.m3u8"), []byte("#EXTM3U"), 0644)
|
|
os.WriteFile(filepath.Join(tmpDir, "init.mp4"), []byte("init"), 0644)
|
|
|
|
err := resetHLSOutput(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("resetHLSOutput error: %v", err)
|
|
}
|
|
|
|
// Verify all cleaned
|
|
for _, name := range []string{"segment_test.ts", "segment_test.m4s", "playlist.m3u8", "init.mp4"} {
|
|
if _, err := os.Stat(filepath.Join(tmpDir, name)); !os.IsNotExist(err) {
|
|
t.Errorf("file %s still exists after reset", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResetHLSOutput_EmptyDir(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
err := resetHLSOutput(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("resetHLSOutput on empty dir error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestActivateSlate_NoSlate(t *testing.T) {
|
|
prevSlateDir := slateDir
|
|
prevHLSDir := hlsDir
|
|
prevMode := playoutMode
|
|
tmpDir := t.TempDir()
|
|
slateDir = filepath.Join(tmpDir, "slate")
|
|
hlsDir = filepath.Join(tmpDir, "hls")
|
|
os.MkdirAll(hlsDir, 0755)
|
|
defer func() {
|
|
slateDir = prevSlateDir
|
|
hlsDir = prevHLSDir
|
|
playoutMode = prevMode
|
|
}()
|
|
|
|
err := activateSlate()
|
|
if err == nil {
|
|
t.Error("expected error when slate HLS missing, got nil")
|
|
}
|
|
}
|
|
|
|
func TestActivateSlate_Success(t *testing.T) {
|
|
prevSlateDir := slateDir
|
|
prevHLSDir := hlsDir
|
|
prevMode := playoutMode
|
|
tmpDir := t.TempDir()
|
|
slateDir = filepath.Join(tmpDir, "slate")
|
|
hlsDir = filepath.Join(tmpDir, "hls")
|
|
|
|
// Create slate HLS structure
|
|
slateHLS := filepath.Join(slateDir, "hls")
|
|
os.MkdirAll(slateHLS, 0755)
|
|
os.MkdirAll(hlsDir, 0755)
|
|
os.WriteFile(filepath.Join(slateHLS, "slate.m3u8"), []byte("#EXTM3U\n#EXT-X-ENDLIST\n"), 0644)
|
|
os.WriteFile(filepath.Join(slateHLS, "slate_segment.ts"), []byte("slate-data"), 0644)
|
|
|
|
defer func() {
|
|
slateDir = prevSlateDir
|
|
hlsDir = prevHLSDir
|
|
playoutMode = prevMode
|
|
}()
|
|
|
|
err := activateSlate()
|
|
if err != nil {
|
|
t.Fatalf("activateSlate error: %v", err)
|
|
}
|
|
|
|
if playoutMode != "slate" {
|
|
t.Errorf("playoutMode = %q, want slate", playoutMode)
|
|
}
|
|
|
|
// Verify files copied
|
|
if _, err := os.Stat(filepath.Join(hlsDir, "slate.m3u8")); os.IsNotExist(err) {
|
|
t.Error("slate.m3u8 not copied to hls dir")
|
|
}
|
|
}
|
|
|
|
func TestDeactivateSlate(t *testing.T) {
|
|
prevMode := playoutMode
|
|
playoutMode = "slate"
|
|
defer func() { playoutMode = prevMode }()
|
|
|
|
deactivateSlate()
|
|
|
|
if playoutMode != "live" {
|
|
t.Errorf("playoutMode = %q, want live", playoutMode)
|
|
}
|
|
}
|
|
|
|
func TestValidatePlaylist(t *testing.T) {
|
|
// ValidatePlaylist calls ValidateVideo which needs ffprobe
|
|
t.Skip("requires ffprobe mock or test media fixtures")
|
|
}
|
|
|
|
func TestSyncToR2(t *testing.T) {
|
|
// Requires R2/S3 mock
|
|
t.Skip("requires S3 client mock")
|
|
} |