346 lines
8.8 KiB
Go
346 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/qvest-digital/go-mxl/mxl"
|
|
|
|
"mxl-pattern-generator/internal/audio"
|
|
"mxl-pattern-generator/internal/flowdef"
|
|
)
|
|
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestAudioBatchSize(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rate mxl.Rational
|
|
want uint64
|
|
}{
|
|
{name: "44.1 kHz", rate: mxl.Rational{Num: 44100, Den: 1}, want: 441},
|
|
{name: "48 kHz", rate: mxl.Rational{Num: 48000, Den: 1}, want: 480},
|
|
{name: "96 kHz", rate: mxl.Rational{Num: 96000, Den: 1}, want: 960},
|
|
{name: "192 kHz", rate: mxl.Rational{Num: 192000, Den: 1}, want: 1920},
|
|
{name: "minimum", rate: mxl.Rational{Num: 1, Den: 1}, want: 1},
|
|
{name: "zero numerator", rate: mxl.Rational{Num: 0, Den: 1}, want: 1},
|
|
{name: "zero denominator", rate: mxl.Rational{Num: 48000, Den: 0}, want: 1},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := audioBatchSize(tc.rate); got != tc.want {
|
|
t.Fatalf("audioBatchSize(%d/%d) = %d, want %d", tc.rate.Num, tc.rate.Den, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunConcurrentCancelsSiblingAndWaitsForCleanup(t *testing.T) {
|
|
wantErr := errors.New("writer failed")
|
|
peerStarted := make(chan struct{})
|
|
peerStopped := make(chan struct{})
|
|
|
|
err := runConcurrent(context.Background(),
|
|
namedRunner{
|
|
name: "video",
|
|
run: func(ctx context.Context) error {
|
|
<-peerStarted
|
|
return wantErr
|
|
},
|
|
},
|
|
namedRunner{
|
|
name: "audio",
|
|
run: func(ctx context.Context) error {
|
|
close(peerStarted)
|
|
<-ctx.Done()
|
|
close(peerStopped)
|
|
return nil
|
|
},
|
|
},
|
|
)
|
|
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("error = %v, want wrapped %v", err, wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), "video flow") {
|
|
t.Fatalf("error = %q, want runner name", err)
|
|
}
|
|
select {
|
|
case <-peerStopped:
|
|
default:
|
|
t.Fatal("runConcurrent returned before the sibling completed cleanup")
|
|
}
|
|
}
|
|
|
|
func TestRunConcurrentParentCancellationIsGraceful(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
started := make(chan struct{})
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- runConcurrent(ctx, namedRunner{
|
|
name: "video",
|
|
run: func(ctx context.Context) error {
|
|
close(started)
|
|
<-ctx.Done()
|
|
return nil
|
|
},
|
|
})
|
|
}()
|
|
|
|
<-started
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("runConcurrent: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("runConcurrent did not stop after parent cancellation")
|
|
}
|
|
}
|