package music import ( "os" "path/filepath" "strings" "testing" ) func TestUniqueNameHandlesShortStemCollisions(t *testing.T) { used := map[string]struct{}{ "mus_wg_mystc": {}, } got := UniqueName("mus_wg_mystc", used) if got != "mus_wg_mystc_1" { t.Fatalf("unexpected collision result: got %q want %q", got, "mus_wg_mystc_1") } } func TestUniqueNameNoCollision(t *testing.T) { used := map[string]struct{}{} got := UniqueName("new_stem", used) if got != "new_stem" { t.Fatalf("expected 'new_stem', got %q", got) } } func TestUniqueNameMultipleCollisions(t *testing.T) { used := map[string]struct{}{ "stem": {}, "stem_1": {}, "stem_2": {}, } got := UniqueName("stem", used) if got != "stem_3" { t.Fatalf("expected 'stem_3', got %q", got) } } func TestGenerateStem(t *testing.T) { used := map[string]struct{}{ "existing": {}, } stem, err := GenerateStem("My Cool Track.mp3", "mus_", used) if err != nil { t.Fatalf("unexpected error: %v", err) } if stem == "" { t.Fatal("expected non-empty stem") } if len(stem) > MaxStemLen { t.Fatalf("stem %q exceeds max length %d", stem, MaxStemLen) } if _, exists := used[stem]; !exists { t.Fatal("expected stem to be registered in used set") } } func TestGenerateStemPrefixTooLong(t *testing.T) { _, err := GenerateStem("test.mp3", "this_prefix_is_way_too_long_for_stem_", nil) if err == nil { t.Fatal("expected error for too-long prefix") } } func TestSlugWords(t *testing.T) { words := SlugWords("My Cool Music Track (Official) [HD]") if len(words) == 0 { t.Fatal("expected non-empty words") } } func TestSlugWordsStripsBrackets(t *testing.T) { words := SlugWords("Song [Explicit] (Remix)") for _, w := range words { if w == "explicit" || w == "remix" { t.Fatalf("bracket content should be removed, got word %q", w) } } } func TestSanitizePrefix(t *testing.T) { if got := SanitizePrefix("Mus_WG_"); got != "mus_wg_" { t.Fatalf("unexpected prefix: %q", got) } if got := SanitizePrefix(" Test "); got != "test" { t.Fatalf("unexpected prefix: %q", got) } if got := SanitizePrefix(""); got != "" { t.Fatalf("expected empty prefix, got %q", got) } } func TestReserveName(t *testing.T) { used := map[string]struct{}{} if err := ReserveName("testname", used); err != nil { t.Fatalf("unexpected error: %v", err) } if _, exists := used["testname"]; !exists { t.Fatal("expected name to be reserved") } if err := ReserveName("testname", used); err == nil { t.Fatal("expected error for duplicate name") } } func TestValidateManualOutputFile(t *testing.T) { if err := ValidateManualOutputFile("test.bmu"); err != nil { t.Fatalf("unexpected error: %v", err) } if err := ValidateManualOutputFile("test.txt"); err == nil { t.Fatal("expected error for non-bmu extension") } if err := ValidateManualOutputFile(".bmu"); err == nil { t.Fatal("expected error for empty stem") } } func TestParseCreditsMarkdownMissingFile(t *testing.T) { entries, err := ParseCreditsMarkdown("nonexistent.md") if err != nil { t.Fatalf("unexpected error for missing file: %v", err) } if entries != nil { t.Fatalf("expected nil entries for missing file, got %v", entries) } } func TestParseCreditsMarkdownRealContent(t *testing.T) { content := "# Header\n\n| Artist | Title | Output File | Original File | Album | Date | License / Copyright | Notes |\n|---|---|---|---|---|---|---|---|\n| Test Artist | Test Title | `output.bmu` | `original.mp3` | Test Album | 2024 | MIT | test |\n" path := filepath.Join(t.TempDir(), "CREDITS.md") if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write test credits: %v", err) } entries, err := ParseCreditsMarkdown(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(entries) != 1 { t.Fatalf("expected 1 entry, got %d", len(entries)) } if entries[0].Artist != "Test Artist" { t.Fatalf("expected 'Test Artist', got %q", entries[0].Artist) } if entries[0].Title != "Test Title" { t.Fatalf("expected 'Test Title', got %q", entries[0].Title) } if entries[0].OutputFile != "output.bmu" { t.Fatalf("expected 'output.bmu', got %q", entries[0].OutputFile) } } func TestRenderCreditsMarkdown(t *testing.T) { entries := []CreditsEntry{ { Artist: "Test Artist", Title: "Test Title", OutputFile: "output.bmu", OriginalFile: "original.mp3", Album: "Test Album", Date: "2024", Rights: "MIT", Notes: "test note", }, } result := RenderCreditsMarkdown(entries) if !strings.Contains(result, "Test Artist") { t.Fatal("expected rendered output to contain artist") } if !strings.Contains(result, "output.bmu") { t.Fatal("expected rendered output to contain output file") } } func TestParseCreditsOverlay(t *testing.T) { content := "# Header\n\n| Artist | Title | Output File | Original File |\n|---|---|---|---|\n| Overlay Artist | Ovr Title | `override.bmu` | `original.mp3` |\n" path := filepath.Join(t.TempDir(), "CREDITS.md") if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatalf("write test overlay: %v", err) } overlay, err := ParseCreditsOverlay(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(overlay.Entries) != 1 { t.Fatalf("expected 1 overlay entry, got %d", len(overlay.Entries)) } matched := overlay.Match("original.mp3", "") if matched == nil { t.Fatal("expected overlay match by original file") } if matched.Artist != "Overlay Artist" { t.Fatalf("expected 'Overlay Artist', got %q", matched.Artist) } } func TestApplyCreditsOverlay(t *testing.T) { entry := &CreditsEntry{ Artist: "Original Artist", Title: "Original Title", } overlay := &CreditsEntry{ Artist: "Overlay Artist", } ApplyCreditsOverlay(entry, overlay) if entry.Artist != "Overlay Artist" { t.Fatalf("expected 'Overlay Artist', got %q", entry.Artist) } if entry.Title != "Original Title" { t.Fatalf("expected 'Original Title' preserved, got %q", entry.Title) } ApplyCreditsOverlay(entry, nil) if entry.Artist != "Overlay Artist" { t.Fatalf("expected nil overlay to be no-op, got %q", entry.Artist) } } func TestValidateOverlayEntries(t *testing.T) { generated := []CreditsEntry{ {OriginalFile: "track1.mp3", OutputFile: "track1.bmu"}, } overlay := CreditsOverlay{ Entries: []CreditsEntry{ {OriginalFile: "track1.mp3"}, }, } if err := ValidateOverlayEntries("test", overlay, generated); err != nil { t.Fatalf("unexpected validation error: %v", err) } badOverlay := CreditsOverlay{ Entries: []CreditsEntry{ {OriginalFile: "nonexistent.mp3"}, }, } if err := ValidateOverlayEntries("test", badOverlay, generated); err == nil { t.Fatal("expected validation error for unknown original file") } } func TestIsMusicAssetPath(t *testing.T) { if !IsMusicAssetPath("envi/music") { t.Fatal("expected 'envi/music' to be music path") } if !IsMusicAssetPath("envi/music/westgate") { t.Fatal("expected 'envi/music/westgate' to be music path") } if IsMusicAssetPath("envi/textures") { t.Fatal("expected 'envi/textures' not to be music path") } if IsMusicAssetPath("") { t.Fatal("expected empty string not to be music path") } } func TestFirstNonEmpty(t *testing.T) { if got := FirstNonEmpty("", "hello", "world"); got != "hello" { t.Fatalf("expected 'hello', got %q", got) } if got := FirstNonEmpty("", "", ""); got != "" { t.Fatalf("expected empty, got %q", got) } if got := FirstNonEmpty("first"); got != "first" { t.Fatalf("expected 'first', got %q", got) } } func TestJoinNonEmpty(t *testing.T) { if got := JoinNonEmpty(", ", "a", "", "b"); got != "a, b" { t.Fatalf("expected 'a, b', got %q", got) } if got := JoinNonEmpty(", "); got != "" { t.Fatalf("expected empty, got %q", got) } } func TestTrimSlashes(t *testing.T) { if got := TrimSlashes("/foo/bar/"); got != "foo/bar" { t.Fatalf("expected 'foo/bar', got %q", got) } if got := TrimSlashes("///"); got != "" { t.Fatalf("expected empty, got %q", got) } } func TestEscapeMarkdownCell(t *testing.T) { result := EscapeMarkdownCell("a|b\nc") if !strings.Contains(result, `\|`) { t.Fatal("expected pipe to be escaped") } if !strings.Contains(result, "
") { t.Fatal("expected newline to be replaced with
") } } func TestWrapCodeCell(t *testing.T) { if got := WrapCodeCell("test.bmu"); got != "`test.bmu`" { t.Fatalf("expected '`test.bmu`', got %q", got) } if got := WrapCodeCell(""); got != "" { t.Fatalf("expected empty string, got %q", got) } } func TestSyncGeneratedCreditsArtifactsWritesNewFiles(t *testing.T) { root := t.TempDir() desired := map[string][]byte{ filepath.Join(root, "test.md"): []byte("content"), } changed, err := SyncGeneratedCreditsArtifacts(root, desired) if err != nil { t.Fatalf("unexpected error: %v", err) } if changed != 1 { t.Fatalf("expected 1 changed file, got %d", changed) } } func TestSyncGeneratedCreditsArtifactsNoChanges(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "test.md") if err := os.WriteFile(path, []byte("content"), 0o644); err != nil { t.Fatalf("write file: %v", err) } desired := map[string][]byte{ path: []byte("content"), } changed, err := SyncGeneratedCreditsArtifacts(root, desired) if err != nil { t.Fatalf("unexpected error: %v", err) } if changed != 0 { t.Fatalf("expected 0 changed files, got %d", changed) } } func TestSyncGeneratedCreditsArtifactsRemovesStale(t *testing.T) { root := t.TempDir() stalePath := filepath.Join(root, "stale.md") if err := os.WriteFile(stalePath, []byte("stale"), 0o644); err != nil { t.Fatalf("write stale file: %v", err) } desired := map[string][]byte{} changed, err := SyncGeneratedCreditsArtifacts(root, desired) if err != nil { t.Fatalf("unexpected error: %v", err) } if changed != 1 { t.Fatalf("expected 1 removed file, got %d", changed) } } func TestPrefixForDir(t *testing.T) { prefixes := map[string]string{ "envi/music/westgate": "mus_wg_", } if got := PrefixForDir(prefixes, "envi/music/westgate"); got != "mus_wg_" { t.Fatalf("expected 'mus_wg_', got %q", got) } if got := PrefixForDir(prefixes, "envi/music/other"); got != "" { t.Fatalf("expected empty prefix, got %q", got) } if got := PrefixForDir(nil, "envi/music"); got != "" { t.Fatalf("expected empty prefix for nil map, got %q", got) } } func TestMaxMin(t *testing.T) { if got := Min(3, 5); got != 3 { t.Fatalf("Min(3,5) = %d, want 3", got) } if got := Max(3, 5); got != 5 { t.Fatalf("Max(3,5) = %d, want 5", got) } } func TestResolveFFmpegEnv(t *testing.T) { t.Setenv("SOW_FFMPEG", "/custom/ffmpeg") path, err := ResolveFFmpeg() if err != nil { t.Fatalf("unexpected error: %v", err) } if path != "/custom/ffmpeg" { t.Fatalf("expected '/custom/ffmpeg', got %q", path) } } func TestResolveFFprobeEnv(t *testing.T) { t.Setenv("SOW_FFPROBE", "/custom/ffprobe") path, err := ResolveFFprobe() if err != nil { t.Fatalf("unexpected error: %v", err) } if path != "/custom/ffprobe" { t.Fatalf("expected '/custom/ffprobe', got %q", path) } } func TestFFmpegMissingErrHasInstallHints(t *testing.T) { msg := ffmpegMissingErr("ffmpeg").Error() for _, want := range []string{"ffmpeg", "winget", "brew", "apt", "SOW_FFMPEG"} { if !strings.Contains(msg, want) { t.Errorf("ffmpeg error missing %q; got:\n%s", want, msg) } } probe := ffmpegMissingErr("ffprobe").Error() if !strings.Contains(probe, "SOW_FFPROBE") { t.Errorf("ffprobe error should mention SOW_FFPROBE; got:\n%s", probe) } }