package main import ( "bytes" "encoding/json" "os" "strings" "testing" "mxl-pattern-generator/internal/audio" "mxl-pattern-generator/internal/flowdef" "mxl-pattern-generator/internal/video" "github.com/spf13/pflag" ) func TestAlphaFlag(t *testing.T) { var args appArgs flags := pflag.NewFlagSet("test", pflag.ContinueOnError) addFlags(flags, &args) if err := flags.Parse([]string{"--alpha"}); err != nil { t.Fatalf("Parse: %v", err) } if !args.videoAlpha { t.Fatal("videoAlpha = false, want true") } } func TestParseArgsHelpStopsBeforeValidation(t *testing.T) { var stdout, stderr bytes.Buffer result, err := parseArgs([]string{"--help"}, &stdout, &stderr) if err != nil { t.Fatalf("parseArgs: %v", err) } if result.shouldRun { t.Fatal("shouldRun = true, want false") } if !result.args.showHelp { t.Fatal("showHelp = false, want true") } if !strings.Contains(stdout.String(), "Usage: mxl-gen") { t.Fatalf("help output does not contain usage: %q", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestParseArgsListPatternsStopsBeforeValidation(t *testing.T) { var stdout, stderr bytes.Buffer result, err := parseArgs([]string{"--list-patterns"}, &stdout, &stderr) if err != nil { t.Fatalf("parseArgs: %v", err) } if result.shouldRun { t.Fatal("shouldRun = true, want false") } if !result.args.listPatterns { t.Fatal("listPatterns = false, want true") } if !strings.Contains(stdout.String(), "ebu75") { t.Fatalf("pattern output does not contain ebu75: %q", stdout.String()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestParseArgsRejectsUnexpectedPositionalArguments(t *testing.T) { var stdout, stderr bytes.Buffer result, err := parseArgs([]string{"unexpected"}, &stdout, &stderr) if err == nil { t.Fatal("parseArgs returned nil error") } if result.shouldRun { t.Fatal("shouldRun = true, want false") } if !strings.Contains(err.Error(), "unexpected positional arguments") { t.Fatalf("error = %q", err) } } func TestParseArgsRejectsUnknownFlag(t *testing.T) { var stdout, stderr bytes.Buffer result, err := parseArgs([]string{"--not-a-flag"}, &stdout, &stderr) if err == nil { t.Fatal("parseArgs returned nil error") } if result.shouldRun { t.Fatal("shouldRun = true, want false") } } func TestValidateAudioArgsSkipsDisabledAudio(t *testing.T) { args := appArgs{ audioChannels: 0, audioSamplingFreq: "unsupported", audioUUID: "not-a-uuid", } if err := validateAudioArgs(&args); err != nil { t.Fatalf("validateAudioArgs: %v", err) } if args.audioUUID != "not-a-uuid" { t.Fatalf("audio UUID changed while audio is disabled: %q", args.audioUUID) } } func TestValidateVideoArgsSkipsDisabledVideo(t *testing.T) { args := appArgs{ noVideo: true, pattern: "not-a-pattern", videoWidth: 1, videoFPS: "unsupported", videoUUID: "not-a-uuid", } if err := validateVideoArgs(&args); err != nil { t.Fatalf("validateVideoArgs: %v", err) } if args.videoUUID != "not-a-uuid" { t.Fatalf("video UUID changed while video is disabled: %q", args.videoUUID) } } func TestParseVideoBackend(t *testing.T) { tests := []struct { name string value string want video.Backend wantErr bool }{ {name: "zero value defaults to auto", want: video.BackendAuto}, {name: "auto", value: "auto", want: video.BackendAuto}, {name: "gpu", value: "gpu", want: video.BackendGPU}, {name: "cpu", value: "cpu", want: video.BackendCPU}, {name: "unknown", value: "other", wantErr: true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := parseVideoBackend(tc.value) if tc.wantErr { if err == nil || !strings.Contains(err.Error(), "unsupported video backend") { t.Fatalf("error = %v, want unsupported backend error", err) } return } if err != nil { t.Fatalf("parseVideoBackend: %v", err) } if got != tc.want { t.Fatalf("backend = %q, want %q", got, tc.want) } }) } } func TestValidateVideoArgsRejectsUnknownBackend(t *testing.T) { args := appArgs{videoBackend: "other"} if err := validateVideoArgs(&args); err == nil || !strings.Contains(err.Error(), "unsupported video backend") { t.Fatalf("error = %v, want unsupported backend error", err) } } func TestValidateMediaSelection(t *testing.T) { tests := []struct { name string args appArgs wantErrSub string }{ { name: "no video with video definition", args: appArgs{noVideo: true, videoFlowDefFile: "video.json", audioChannels: 2}, wantErrSub: "cannot be used with --video", }, { name: "no flows", args: appArgs{noVideo: true}, wantErrSub: "requires audio enabled", }, { name: "generated audio only", args: appArgs{noVideo: true, audioChannels: 2}, }, { name: "external audio only", args: appArgs{noVideo: true, audioFlowDefFile: "audio.json"}, }, { name: "video enabled by default", args: appArgs{}, }, { name: "alpha without video", args: appArgs{noVideo: true, videoAlpha: true, audioChannels: 2}, wantErrSub: "cannot be used with --no-video", }, { name: "alpha with custom video definition", args: appArgs{videoAlpha: true, videoFlowDefFile: "video.json"}, wantErrSub: "cannot be used with --video", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { err := validateMediaSelection(tc.args) if tc.wantErrSub == "" { if err != nil { t.Fatalf("validateMediaSelection: %v", err) } return } if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) { t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub) } }) } } func TestListPatternsIsSorted(t *testing.T) { var output bytes.Buffer listPatterns(&output) text := output.String() if strings.Index(text, "ebu100") > strings.Index(text, "ebu75") { t.Fatalf("patterns are not sorted: %q", text) } } func TestBuildVideoConfigFromArgs(t *testing.T) { args := appArgs{ videoUUID: "5fbec3b1-1b0f-417d-9059-8b94a47197ed", videoWidth: 1920, videoHeight: 1080, videoFPS: "29.97", pattern: "ebu75", } cfg, err := buildVideoConfig(args) if err != nil { t.Fatalf("buildVideoConfig: %v", err) } if cfg.Width() != 1920 || cfg.Height() != 1080 { t.Fatalf("dimensions = %dx%d, want 1920x1080", cfg.Width(), cfg.Height()) } if cfg.Rate().Num != 30000 || cfg.Rate().Den != 1001 { t.Fatalf("rate = %d/%d, want 30000/1001", cfg.Rate().Num, cfg.Rate().Den) } if cfg.Backend != video.BackendAuto { t.Fatalf("backend = %q, want %q", cfg.Backend, video.BackendAuto) } } func TestBuildVideoConfigWithAlpha(t *testing.T) { cfg, err := buildVideoConfig(appArgs{ videoUUID: "5fbec3b1-1b0f-417d-9059-8b94a47197ed", videoWidth: 1280, videoHeight: 720, videoFPS: "50", pattern: "gray-ramp", videoAlpha: true, }) if err != nil { t.Fatalf("buildVideoConfig: %v", err) } if cfg.Definition.MediaType != flowdef.MediaTypeV210A { t.Fatalf("media type = %q, want %q", cfg.Definition.MediaType, flowdef.MediaTypeV210A) } if !cfg.HasAlpha() { t.Fatal("HasAlpha() = false, want true") } } func TestBuildVideoConfigBackends(t *testing.T) { for _, backend := range []video.Backend{video.BackendGPU, video.BackendCPU} { t.Run(string(backend), func(t *testing.T) { cfg, err := buildVideoConfig(appArgs{ videoUUID: "5fbec3b1-1b0f-417d-9059-8b94a47197ed", videoWidth: 1920, videoHeight: 1080, videoFPS: "25", pattern: "ebu75", videoBackend: string(backend), }) if err != nil { t.Fatalf("buildVideoConfig: %v", err) } if cfg.Backend != backend { t.Fatalf("backend = %q, want %q", cfg.Backend, backend) } }) } } func TestBuildVideoConfigDisabled(t *testing.T) { cfg, err := buildVideoConfig(appArgs{noVideo: true}) if err != nil { t.Fatalf("buildVideoConfig: %v", err) } if cfg != nil { t.Fatalf("config = %+v, want nil for disabled video", cfg) } } func TestBuildVideoConfigFromFile(t *testing.T) { definition, err := flowdef.NewV210Video( "5fbec3b1-1b0f-417d-9059-8b94a47197ed", 3840, 2160, flowdef.Rational{Numerator: 60000, Denominator: 1001}, ) if err != nil { t.Fatalf("NewV210Video: %v", err) } data, err := json.Marshal(definition) if err != nil { t.Fatalf("json.Marshal: %v", err) } path := t.TempDir() + "/video.json" if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatalf("os.WriteFile: %v", err) } cfg, err := buildVideoConfig(appArgs{ videoFlowDefFile: path, pattern: "smpte", // These values must be ignored when a definition file is supplied. videoWidth: 1920, videoHeight: 1080, videoFPS: "25", }) if err != nil { t.Fatalf("buildVideoConfig: %v", err) } if cfg.Width() != 3840 || cfg.Height() != 2160 { t.Fatalf("dimensions = %dx%d, want file values 3840x2160", cfg.Width(), cfg.Height()) } if cfg.Rate().Num != 60000 || cfg.Rate().Den != 1001 { t.Fatalf("rate = %d/%d, want file value 60000/1001", cfg.Rate().Num, cfg.Rate().Den) } } func TestBuildAudioConfigDisabled(t *testing.T) { cfg, err := buildAudioConfig(appArgs{}) if err != nil { t.Fatalf("buildAudioConfig: %v", err) } if cfg != nil { t.Fatalf("config = %+v, want nil for disabled audio", cfg) } } func TestBuildAudioConfigFromArgs(t *testing.T) { cfg, err := buildAudioConfig(appArgs{ audioUUID: "b3bb5be7-9fe9-4324-a5bb-4c70e1084449", audioChannels: 2, audioSamplingFreq: "48", audioLevel: "ebu", }) if err != nil { t.Fatalf("buildAudioConfig: %v", err) } if cfg == nil { t.Fatal("config is nil") } if cfg.Channels() != 2 { t.Fatalf("channels = %d, want 2", cfg.Channels()) } if cfg.Rate().Num != 48000 || cfg.Rate().Den != 1 { t.Fatalf("rate = %d/%d, want 48000/1", cfg.Rate().Num, cfg.Rate().Den) } if cfg.LevelDBFS != audio.LevelEBUDBFS { t.Fatalf("level = %.2f dBFS, want %.2f dBFS", cfg.LevelDBFS, audio.LevelEBUDBFS) } } func TestBuildAudioConfigFromFile(t *testing.T) { definition, err := flowdef.NewFloat32Audio( "b3bb5be7-9fe9-4324-a5bb-4c70e1084449", 8, flowdef.Rational{Numerator: 96000, Denominator: 1}, ) if err != nil { t.Fatalf("NewFloat32Audio: %v", err) } data, err := json.Marshal(definition) if err != nil { t.Fatalf("json.Marshal: %v", err) } path := t.TempDir() + "/audio.json" if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatalf("os.WriteFile: %v", err) } cfg, err := buildAudioConfig(appArgs{ audioFlowDefFile: path, audioChannels: 2, audioSamplingFreq: "48", audioLevel: "smpte", }) if err != nil { t.Fatalf("buildAudioConfig: %v", err) } if cfg == nil { t.Fatal("config is nil") } if cfg.Channels() != 8 { t.Fatalf("channels = %d, want file value 8", cfg.Channels()) } if cfg.Rate().Num != 96000 || cfg.Rate().Den != 1 { t.Fatalf("rate = %d/%d, want file value 96000/1", cfg.Rate().Num, cfg.Rate().Den) } if cfg.LevelDBFS != audio.LevelSMPTEDBFS { t.Fatalf("level = %.2f dBFS, want %.2f dBFS", cfg.LevelDBFS, audio.LevelSMPTEDBFS) } } func TestValidateAudioArgsRejectsUnknownLevelForFlowDefinition(t *testing.T) { args := appArgs{ audioFlowDefFile: "audio.json", audioLevel: "unknown", } err := validateAudioArgs(&args) if err == nil || !strings.Contains(err.Error(), "unsupported audio level") { t.Fatalf("error = %v, want unsupported audio level error", err) } }