Files
GVASTE d6c50ed623 windows: fix config, IPC, path, and Lua compatibility (#258)
* windows: fix config, IPC, path, and Lua compatibility

* spotify: share API structures and helper so tests compile on Windows

* windows: address PR comments and fix socket unavailable detection, tasklist matching, api_fs tests, and doc comment

* windows: address remaining PR reviews (table-driven tests, m3u resolve comments & tests, blockquote formatting, and site/index.html description)

* feat: implement cross-platform IPC server with Unix and Windows support

* feat: implement Unix socket IPC server and Windows process liveness check

* ipc: detect dead processes via os.ErrProcessDone

os.Process.Signal converts ESRCH to os.ErrProcessDone since Go 1.16, so
comparing against raw syscall.ESRCH never matched and a stale socket from
a crashed instance made NewServer fail instead of cleaning it up.

* windows: simplify fs allowlist normalization, exec env, and ipc error checks

- normalize write allow-dirs once in the memoized writeAllowDirs
- collapse duplicate test helpers and repeated getenv blocks
- drop redundant errors.As branch; name WSAECONNREFUSED
- revert single-entry table test to linear form
- gofmt: trailing newlines and indentation

---------

Co-authored-by: Bjarne Øverli <bjarne.oeverli@gmail.com>
2026-06-04 19:52:32 +02:00

233 lines
5.7 KiB
Go

package resolve
import (
"path/filepath"
"strings"
"testing"
)
func TestParseM3UBasic(t *testing.T) {
input := `#EXTM3U
#EXTINF:120,Artist - Song One
http://example.com/song1.mp3
#EXTINF:180,Artist - Song Two
http://example.com/song2.mp3
`
entries, err := parseM3U(strings.NewReader(input), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 2 {
t.Fatalf("got %d entries, want 2", len(entries))
}
if entries[0].Title != "Artist - Song One" {
t.Errorf("entry[0].Title = %q, want %q", entries[0].Title, "Artist - Song One")
}
if entries[0].Duration != 120 {
t.Errorf("entry[0].Duration = %d, want 120", entries[0].Duration)
}
if entries[0].Path != "http://example.com/song1.mp3" {
t.Errorf("entry[0].Path = %q", entries[0].Path)
}
if entries[1].Title != "Artist - Song Two" {
t.Errorf("entry[1].Title = %q", entries[1].Title)
}
if entries[1].Duration != 180 {
t.Errorf("entry[1].Duration = %d, want 180", entries[1].Duration)
}
}
func TestParseM3UNoHeader(t *testing.T) {
// M3U without #EXTM3U header should still work
input := `http://example.com/stream1.mp3
http://example.com/stream2.mp3
`
entries, err := parseM3U(strings.NewReader(input), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 2 {
t.Fatalf("got %d entries, want 2", len(entries))
}
if entries[0].Duration != -1 {
t.Errorf("bare entry Duration = %d, want -1", entries[0].Duration)
}
}
func TestParseM3UBOM(t *testing.T) {
// UTF-8 BOM prefix should be stripped
input := "\xef\xbb\xbf#EXTM3U\n#EXTINF:60,Song\nhttp://example.com/song.mp3\n"
entries, err := parseM3U(strings.NewReader(input), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
if entries[0].Title != "Song" {
t.Errorf("Title = %q, want Song", entries[0].Title)
}
}
func TestParseM3USkipsComments(t *testing.T) {
input := `#EXTM3U
# This is a comment
#EXTINF:60,Song
http://example.com/song.mp3
#EXTVLCOPT:some-option
`
entries, err := parseM3U(strings.NewReader(input), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
}
func TestParseM3URelativePaths(t *testing.T) {
input := `#EXTM3U
#EXTINF:60,Song
music/song.mp3
`
entries, err := parseM3U(strings.NewReader(input), "/home/user")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
if entries[0].Path != "/home/user/music/song.mp3" {
t.Errorf("Path = %q, want /home/user/music/song.mp3", entries[0].Path)
}
}
func TestParseM3UAbsolutePaths(t *testing.T) {
input := "/absolute/path/song.mp3\n"
entries, err := parseM3U(strings.NewReader(input), "/home/user")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if entries[0].Path != "/absolute/path/song.mp3" {
t.Errorf("Path = %q, want /absolute/path/song.mp3", entries[0].Path)
}
}
func TestParseM3UEmpty(t *testing.T) {
entries, err := parseM3U(strings.NewReader(""), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 0 {
t.Fatalf("got %d entries, want 0", len(entries))
}
}
func TestParseM3URadioStream(t *testing.T) {
// Radio stream with -1 duration
input := `#EXTM3U
#EXTINF:-1,Radio Station
http://radio.example.com/stream
`
entries, err := parseM3U(strings.NewReader(input), "")
if err != nil {
t.Fatalf("parseM3U error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
if entries[0].Duration != -1 {
t.Errorf("Duration = %d, want -1", entries[0].Duration)
}
if entries[0].Title != "Radio Station" {
t.Errorf("Title = %q, want Radio Station", entries[0].Title)
}
}
func TestM3UEntryToTrackStream(t *testing.T) {
e := m3uEntry{
Path: "http://radio.example.com/stream",
Title: "Radio Station",
Duration: -1,
}
tr := m3uEntryToTrack(e)
if !tr.Stream {
t.Error("Stream should be true for URL")
}
if !tr.Realtime {
t.Error("Realtime should be true for stream with -1 duration")
}
if tr.DurationSecs != 0 {
t.Errorf("DurationSecs = %d, want 0 (negative clamped)", tr.DurationSecs)
}
if tr.Title != "Radio Station" {
t.Errorf("Title = %q, want Radio Station", tr.Title)
}
}
func TestM3UEntryToTrackFile(t *testing.T) {
e := m3uEntry{
Path: "/home/user/song.mp3",
Title: "My Song",
Duration: 180,
}
tr := m3uEntryToTrack(e)
if tr.Stream {
t.Error("Stream should be false for local file")
}
if tr.Realtime {
t.Error("Realtime should be false for local file")
}
if tr.DurationSecs != 180 {
t.Errorf("DurationSecs = %d, want 180", tr.DurationSecs)
}
}
func TestResolveM3UPathWindows(t *testing.T) {
tests := []struct {
name string
baseDir string
entry string
want string
}{
{
name: "relative backslash",
baseDir: `C:\Music`,
entry: `artist\song.mp3`,
want: filepath.Clean(filepath.Join(`C:\Music`, filepath.FromSlash(`artist\song.mp3`))),
},
{
name: "relative forward slash",
baseDir: `C:\Music`,
entry: `artist/song.mp3`,
want: filepath.Clean(filepath.Join(`C:\Music`, filepath.FromSlash(`artist/song.mp3`))),
},
{
name: "absolute drive-letter",
baseDir: `C:\Music`,
entry: `D:\Other\track.mp3`,
want: filepath.Clean(`D:\Other\track.mp3`),
},
{
name: "absolute UNC path",
baseDir: `\\server\share`,
entry: `sub\file.mp3`,
want: filepath.Clean(filepath.Join(`\\server\share`, filepath.FromSlash(`sub\file.mp3`))),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveM3UPath(tt.baseDir, tt.entry)
if got != tt.want {
t.Fatalf("resolveM3UPath(%q, %q) = %q, want %q", tt.baseDir, tt.entry, got, tt.want)
}
})
}
}