Files
Dmitry Sergeev dea2e6a59f alpha patterns
2026-09-18 10:11:07 +03:00

435 lines
11 KiB
Go

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)
}
}