502 lines
13 KiB
Go
502 lines
13 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bjarneo/cliamp/playlist"
|
|
)
|
|
|
|
// captureStdout runs fn with os.Stdout redirected to a buffer and returns what
|
|
// was written.
|
|
func captureStdout(t *testing.T, fn func() error) (string, error) {
|
|
t.Helper()
|
|
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatalf("os.Pipe: %v", err)
|
|
}
|
|
old := os.Stdout
|
|
os.Stdout = w
|
|
|
|
done := make(chan struct{})
|
|
var buf bytes.Buffer
|
|
go func() {
|
|
_, _ = io.Copy(&buf, r)
|
|
close(done)
|
|
}()
|
|
|
|
runErr := fn()
|
|
w.Close()
|
|
<-done
|
|
os.Stdout = old
|
|
return buf.String(), runErr
|
|
}
|
|
|
|
func writeAudioFile(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, []byte{}, 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
}
|
|
|
|
func setupTestEnv(t *testing.T) string {
|
|
t.Helper()
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
return home
|
|
}
|
|
|
|
func TestPlaylistListEmpty(t *testing.T) {
|
|
setupTestEnv(t)
|
|
|
|
out, err := captureStdout(t, PlaylistList)
|
|
if err != nil {
|
|
t.Fatalf("PlaylistList: %v", err)
|
|
}
|
|
if !strings.Contains(out, "No playlists") {
|
|
t.Errorf("output = %q, want 'No playlists...'", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistCreateAndList(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audioDir := filepath.Join(home, "music")
|
|
writeAudioFile(t, filepath.Join(audioDir, "song1.mp3"))
|
|
writeAudioFile(t, filepath.Join(audioDir, "song2.flac"))
|
|
|
|
// Create
|
|
out, err := captureStdout(t, func() error {
|
|
return PlaylistCreate("mymix", []string{audioDir}, "")
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
if !strings.Contains(out, "Created playlist") {
|
|
t.Errorf("create output = %q, want 'Created playlist'", out)
|
|
}
|
|
|
|
// List
|
|
out, err = captureStdout(t, PlaylistList)
|
|
if err != nil {
|
|
t.Fatalf("PlaylistList: %v", err)
|
|
}
|
|
if !strings.Contains(out, "mymix") {
|
|
t.Errorf("list output = %q, want to mention 'mymix'", out)
|
|
}
|
|
if !strings.Contains(out, "2 tracks") {
|
|
t.Errorf("list output = %q, want '2 tracks'", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistCreateNoAudio(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
emptyDir := filepath.Join(home, "empty")
|
|
if err := os.MkdirAll(emptyDir, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
|
|
err := PlaylistCreate("nothing", []string{emptyDir}, "")
|
|
if err == nil {
|
|
t.Fatal("PlaylistCreate with no audio should error")
|
|
}
|
|
if !strings.Contains(err.Error(), "no audio") {
|
|
t.Errorf("error = %q, want to mention 'no audio'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPlaylistCreateEmpty(t *testing.T) {
|
|
setupTestEnv(t)
|
|
|
|
out, err := captureStdout(t, func() error {
|
|
return PlaylistCreate("empty", nil, "")
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PlaylistCreate empty: %v", err)
|
|
}
|
|
if !strings.Contains(out, "Created empty playlist") {
|
|
t.Fatalf("output = %q, want empty playlist confirmation", out)
|
|
}
|
|
out, err = captureStdout(t, func() error { return PlaylistShow("empty", false) })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistShow empty: %v", err)
|
|
}
|
|
if !strings.Contains(out, "is empty") {
|
|
t.Fatalf("show output = %q, want empty message", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistCreateDuplicate(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audio := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, audio)
|
|
|
|
if err := PlaylistCreate("dup", []string{audio}, ""); err != nil {
|
|
t.Fatalf("first PlaylistCreate: %v", err)
|
|
}
|
|
err := PlaylistCreate("dup", []string{audio}, "")
|
|
if err == nil {
|
|
t.Error("duplicate create should error")
|
|
}
|
|
if !strings.Contains(err.Error(), "already exists") {
|
|
t.Errorf("error = %q, want to mention 'already exists'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPlaylistAddAppends(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
b := filepath.Join(home, "b.mp3")
|
|
writeAudioFile(t, a)
|
|
writeAudioFile(t, b)
|
|
|
|
if err := PlaylistCreate("mix", []string{a}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
if err := PlaylistAdd("mix", []string{b}); err != nil {
|
|
t.Fatalf("PlaylistAdd: %v", err)
|
|
}
|
|
|
|
out, _ := captureStdout(t, func() error { return PlaylistShow("mix", false) })
|
|
if !strings.Contains(out, "2 tracks") {
|
|
t.Errorf("Show output = %q, want '2 tracks' after add", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistAddSkipsDuplicates(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, a)
|
|
|
|
if err := PlaylistCreate("mix", []string{a}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
out, err := captureStdout(t, func() error { return PlaylistAdd("mix", []string{a}) })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistAdd duplicate: %v", err)
|
|
}
|
|
if !strings.Contains(out, "0 tracks") || !strings.Contains(out, "1 duplicate skipped") {
|
|
t.Fatalf("output = %q, want duplicate skip count", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistAddNonExistent(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, a)
|
|
|
|
err := PlaylistAdd("ghost", []string{a})
|
|
if err == nil {
|
|
t.Error("PlaylistAdd on non-existent playlist should error")
|
|
}
|
|
}
|
|
|
|
func TestPlaylistShowEmpty(t *testing.T) {
|
|
setupTestEnv(t)
|
|
err := PlaylistShow("ghost", false)
|
|
if err == nil {
|
|
t.Error("PlaylistShow of missing playlist should error")
|
|
}
|
|
}
|
|
|
|
func TestPlaylistShowJSON(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audio := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, audio)
|
|
if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
out, err := captureStdout(t, func() error { return PlaylistShow("mix", true) })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistShow JSON: %v", err)
|
|
}
|
|
if !strings.HasPrefix(strings.TrimSpace(out), "[") {
|
|
t.Errorf("JSON output should start with '[': %s", out)
|
|
}
|
|
if !strings.Contains(out, "\"path\"") {
|
|
t.Errorf("JSON output should contain 'path' key: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistRemove(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
b := filepath.Join(home, "b.mp3")
|
|
writeAudioFile(t, a)
|
|
writeAudioFile(t, b)
|
|
if err := PlaylistCreate("mix", []string{a, b}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
if err := PlaylistRemove("mix", 1); err != nil {
|
|
t.Fatalf("PlaylistRemove: %v", err)
|
|
}
|
|
|
|
out, _ := captureStdout(t, func() error { return PlaylistShow("mix", false) })
|
|
if !strings.Contains(out, "1 tracks") {
|
|
t.Errorf("output = %q, want '1 tracks' after remove", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistRemoveOutOfRange(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, a)
|
|
if err := PlaylistCreate("mix", []string{a}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
err := PlaylistRemove("mix", 999)
|
|
if err == nil {
|
|
t.Error("PlaylistRemove with out-of-range index should error")
|
|
}
|
|
}
|
|
|
|
func TestPlaylistDelete(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audio := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, audio)
|
|
if err := PlaylistCreate("todelete", []string{audio}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
if err := PlaylistDelete("todelete"); err != nil {
|
|
t.Fatalf("PlaylistDelete: %v", err)
|
|
}
|
|
|
|
out, _ := captureStdout(t, PlaylistList)
|
|
if strings.Contains(out, "todelete") {
|
|
t.Errorf("after delete, List output still contains 'todelete': %s", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistDeleteMissing(t *testing.T) {
|
|
setupTestEnv(t)
|
|
err := PlaylistDelete("ghost")
|
|
if err == nil {
|
|
t.Error("PlaylistDelete on missing playlist should error")
|
|
}
|
|
}
|
|
|
|
func TestPlaylistRename(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, a)
|
|
if err := PlaylistCreate("old", []string{a}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
if err := PlaylistRename("old", "new"); err != nil {
|
|
t.Fatalf("PlaylistRename: %v", err)
|
|
}
|
|
if err := PlaylistShow("new", false); err != nil {
|
|
t.Fatalf("renamed playlist missing: %v", err)
|
|
}
|
|
if err := PlaylistShow("old", false); err == nil {
|
|
t.Fatal("old playlist should no longer exist")
|
|
}
|
|
}
|
|
|
|
func TestPlaylistDedupeAndSort(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
b := filepath.Join(home, "b.mp3")
|
|
writeAudioFile(t, a)
|
|
writeAudioFile(t, b)
|
|
if err := PlaylistCreate("mix", nil, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate empty: %v", err)
|
|
}
|
|
p, err := newProvider()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.SavePlaylist("mix", []playlist.Track{{Path: b, Title: "B"}, {Path: a, Title: "A"}, {Path: a, Title: "A dup"}}); err != nil {
|
|
t.Fatalf("SavePlaylist: %v", err)
|
|
}
|
|
if err := PlaylistDedupe("mix"); err != nil {
|
|
t.Fatalf("PlaylistDedupe: %v", err)
|
|
}
|
|
if err := PlaylistSort("mix", "title"); err != nil {
|
|
t.Fatalf("PlaylistSort: %v", err)
|
|
}
|
|
tracks, err := p.Tracks("mix")
|
|
if err != nil {
|
|
t.Fatalf("Tracks: %v", err)
|
|
}
|
|
if len(tracks) != 2 || tracks[0].Title != "A" || tracks[1].Title != "B" {
|
|
t.Fatalf("tracks after dedupe/sort = %+v", tracks)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistDoctorFixPrunesMissing(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
missing := filepath.Join(home, "missing.mp3")
|
|
writeAudioFile(t, a)
|
|
if err := PlaylistCreate("mix", nil, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate empty: %v", err)
|
|
}
|
|
p, err := newProvider()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.SavePlaylist("mix", []playlist.Track{{Path: a, Title: "A"}, {Path: missing, Title: "Missing"}}); err != nil {
|
|
t.Fatalf("SavePlaylist: %v", err)
|
|
}
|
|
if err := PlaylistDoctor("mix", true); err != nil {
|
|
t.Fatalf("PlaylistDoctor: %v", err)
|
|
}
|
|
tracks, err := p.Tracks("mix")
|
|
if err != nil {
|
|
t.Fatalf("Tracks: %v", err)
|
|
}
|
|
if len(tracks) != 1 || tracks[0].Path != a {
|
|
t.Fatalf("tracks after doctor = %+v", tracks)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistImportExportM3U(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
a := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, a)
|
|
m3u := filepath.Join(home, "mix.m3u")
|
|
if err := os.WriteFile(m3u, []byte("#EXTM3U\n#EXTINF:12,Song\na.mp3\n"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
if err := PlaylistImport(m3u, "imported"); err != nil {
|
|
t.Fatalf("PlaylistImport: %v", err)
|
|
}
|
|
out, err := captureStdout(t, func() error { return PlaylistExport("imported", "m3u", "") })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistExport: %v", err)
|
|
}
|
|
if !strings.Contains(out, "#EXTM3U") || !strings.Contains(out, a) {
|
|
t.Fatalf("export output = %q, want M3U with resolved path", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistExportInvalidFormatDoesNotTruncateOutput(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
p, err := newProvider()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.SavePlaylist("mix", []playlist.Track{{Path: "/a.mp3", Title: "A"}}); err != nil {
|
|
t.Fatalf("SavePlaylist: %v", err)
|
|
}
|
|
out := filepath.Join(home, "keep.txt")
|
|
if err := os.WriteFile(out, []byte("keep"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
if err := PlaylistExport("mix", "bad", out); err == nil {
|
|
t.Fatal("PlaylistExport should reject unsupported format")
|
|
}
|
|
data, err := os.ReadFile(out)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(data) != "keep" {
|
|
t.Fatalf("output file = %q, want keep", string(data))
|
|
}
|
|
}
|
|
|
|
func TestPlaylistBookmarkToggle(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audio := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, audio)
|
|
if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
|
|
// Bookmark on.
|
|
out, err := captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistBookmark: %v", err)
|
|
}
|
|
if !strings.Contains(out, "★") {
|
|
t.Errorf("first bookmark output = %q, want '★'", out)
|
|
}
|
|
|
|
// Bookmark off.
|
|
out, err = captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })
|
|
if err != nil {
|
|
t.Fatalf("PlaylistBookmark toggle: %v", err)
|
|
}
|
|
if !strings.Contains(out, "☆") {
|
|
t.Errorf("second bookmark output = %q, want '☆'", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistBookmarksEmpty(t *testing.T) {
|
|
setupTestEnv(t)
|
|
out, err := captureStdout(t, PlaylistBookmarks)
|
|
if err != nil {
|
|
t.Fatalf("PlaylistBookmarks: %v", err)
|
|
}
|
|
if !strings.Contains(out, "No bookmarks") {
|
|
t.Errorf("output = %q, want 'No bookmarks...'", out)
|
|
}
|
|
}
|
|
|
|
func TestPlaylistBookmarksShowsStars(t *testing.T) {
|
|
home := setupTestEnv(t)
|
|
audio := filepath.Join(home, "a.mp3")
|
|
writeAudioFile(t, audio)
|
|
if err := PlaylistCreate("mix", []string{audio}, ""); err != nil {
|
|
t.Fatalf("PlaylistCreate: %v", err)
|
|
}
|
|
// Bookmark (captureStdout silences the toggle output).
|
|
_, _ = captureStdout(t, func() error { return PlaylistBookmark("mix", 1) })
|
|
|
|
out, err := captureStdout(t, PlaylistBookmarks)
|
|
if err != nil {
|
|
t.Fatalf("PlaylistBookmarks: %v", err)
|
|
}
|
|
if !strings.Contains(out, "★") {
|
|
t.Errorf("bookmarks output = %q, want '★'", out)
|
|
}
|
|
}
|
|
|
|
func TestCollectLocalAudioMultiplePaths(t *testing.T) {
|
|
dir := t.TempDir()
|
|
a := filepath.Join(dir, "a.mp3")
|
|
b := filepath.Join(dir, "sub", "b.flac")
|
|
writeAudioFile(t, a)
|
|
writeAudioFile(t, b)
|
|
|
|
got, err := collectLocalAudio([]string{a, b})
|
|
if err != nil {
|
|
t.Fatalf("collectLocalAudio: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Errorf("got %d paths, want 2 — %v", len(got), got)
|
|
}
|
|
}
|
|
|
|
func TestCollectLocalAudioMissingPath(t *testing.T) {
|
|
_, err := collectLocalAudio([]string{filepath.Join(t.TempDir(), "nope")})
|
|
if err == nil {
|
|
t.Error("collectLocalAudio with missing path should error")
|
|
}
|
|
}
|
|
|
|
func TestNewProvider(t *testing.T) {
|
|
setupTestEnv(t)
|
|
p, err := newProvider()
|
|
if err != nil {
|
|
t.Fatalf("newProvider: %v", err)
|
|
}
|
|
if p == nil {
|
|
t.Error("newProvider returned nil provider")
|
|
}
|
|
}
|