diff --git a/commander_test.go b/commander_test.go new file mode 100644 index 0000000..2b00594 --- /dev/null +++ b/commander_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "runtime" + "testing" +) + +func TestIsInstalled_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast isInstalled unit test") + + tests := []struct { + name string + cmd string + want bool + }{ + {"sh exists", "sh", true}, + {"non-existent", "nonexistentcmd123456789", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isInstalled(tt.cmd) + if got != tt.want { + t.Errorf("isInstalled(%q) = %v, want %v", tt.cmd, got, tt.want) + } + }) + } +} + +func TestIsInstalled_Live(t *testing.T) { + if !testing.Short() { + t.Skip("skipping live integration test. Run with:\n go test -run TestIsInstalled_Live -short -v") + } + t.Log("πŸ§ͺ Running live isInstalled integration test...") + + if runtime.GOOS == "windows" { + t.Skip("isInstalled uses /bin/sh (Unix-only)") + } + + if !isInstalled("sh") { + t.Error("sh should be installed on this system") + } + t.Log("βœ“ Live isInstalled test passed") +} + +func TestSubExecute_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast subExecute unit test") + + _, err := subExecute("nonexistentcmd123456789") + if err == nil { + t.Error("expected error for non-existent command") + } +} + +func TestSubExecute_Live(t *testing.T) { + if !testing.Short() { + t.Skip("skipping live integration test. Run with:\n go test -run TestSubExecute_Live -short -v") + } + t.Log("πŸ§ͺ Running live subExecute integration test...") + + output, err := subExecute("echo", "hello from live test") + if err != nil { + t.Fatalf("subExecute failed: %v", err) + } + if len(output) == 0 { + t.Error("expected output") + } + t.Log("βœ“ subExecute live test passed") +} diff --git a/filer_test.go b/filer_test.go new file mode 100644 index 0000000..8106a34 --- /dev/null +++ b/filer_test.go @@ -0,0 +1,140 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCreateIniFile_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast createIniFile unit test") + + tests := []struct { + name string + fpath string + setup func(string) + wantLen int + wantFile bool + }{ + { + name: "happy path", + fpath: "testdata/config.ini", + wantLen: 0, + wantFile: true, + }, + { + name: "dir already exists", + fpath: "testdata/config.ini", + setup: func(dir string) { + os.MkdirAll(dir, 0770) + }, + wantLen: 0, + wantFile: true, + }, + { + name: "deep nested dir", + fpath: "testdata/nested/sub/dir/config.ini", + wantLen: 0, + wantFile: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + fpath := filepath.Join(dir, tt.fpath) + + origWd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working dir: %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("failed to chdir: %v", err) + } + defer func() { + if err := os.Chdir(origWd); err != nil { + t.Errorf("failed to restore working dir: %v", err) + } + }() + + if tt.setup != nil { + tt.setup(filepath.Dir(fpath)) + } + + errs := createIniFile(fpath) + if len(errs) != tt.wantLen { + t.Errorf("createIniFile() got %d errors = %v, want len %d", len(errs), errs, tt.wantLen) + } + + exists := false + if tt.wantFile { + exists = true + if _, err := os.Stat(fpath); err != nil { + if os.IsNotExist(err) { + exists = false + } else { + t.Errorf("stat(%q) unexpected error: %v", fpath, err) + } + } + } + + if exists != tt.wantFile { + t.Errorf("file exists = %v, want %v", exists, tt.wantFile) + } + + if tt.wantFile { + data, err := os.ReadFile(fpath) + if err != nil { + t.Errorf("failed to read created file: %v", err) + return + } + const wantContent = `[DEFAULT] +radio_browser.api=all.api.radio-browser.info +player.command=mpv +player.options=--no-video +menu_items.max=9999 +` + if string(data) != wantContent { + t.Errorf("file content mismatch:\ngot: %q\nwant: %q", string(data), wantContent) + } + } + }) + } +} + +func TestCreateIniFile_Live(t *testing.T) { + if !testing.Short() { + t.Skip("skipping live integration test. Run with:\n go test -run TestCreateIniFile_Live -short -v") + } + t.Log("πŸ§ͺ Running live integration test...") + + dir := t.TempDir() + fpath := filepath.Join(dir, "live_config.ini") + t.Logf("πŸ§ͺ Testing with real file path: %s", fpath) + + errs := createIniFile(fpath) + t.Logf("πŸ§ͺ createIniFile returned %d errors", len(errs)) + + if len(errs) > 0 { + t.Errorf("unexpected errors: %v", errs) + return + } + + data, err := os.ReadFile(fpath) + if err != nil { + t.Fatalf("failed to read created file: %v", err) + } + + const wantContent = `[DEFAULT] +radio_browser.api=all.api.radio-browser.info +player.command=mpv +player.options=--no-video +menu_items.max=9999 +` + if string(data) != wantContent { + t.Errorf("file content mismatch:\ngot: %q\nwant: %q", string(data), wantContent) + } + + t.Logf("πŸ§ͺ Live test PASSED: file created successfully with correct content") +} \ No newline at end of file diff --git a/radiomenu_test.go b/radiomenu_test.go new file mode 100644 index 0000000..9ae5afb --- /dev/null +++ b/radiomenu_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestShort_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast Short unit test") + + tests := []struct { + name string + input string + maxLen int + expected string + }{ + { + name: "short_string", + input: "abc", + maxLen: 5, + expected: "abc", + }, + { + name: "exact_length", + input: "abcde", + maxLen: 5, + expected: "abcde", + }, + { + name: "truncate_long", + input: "this is a very long string that needs truncation", + maxLen: 10, + expected: "this is a ", + }, + { + name: "truncate_unicode", + input: "γ“γ‚“γ«γ‘γ―δΈ–η•Œ", + maxLen: 5, + expected: "こんにけは", + }, + { + name: "zero_length", + input: "hello", + maxLen: 0, + expected: "", + }, + { + name: "empty_string", + input: "", + maxLen: 10, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Short(tt.input, tt.maxLen) + if result != tt.expected { + t.Errorf("Short(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected) + } + }) + } +} + +func TestQuit_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast Quit unit test") + + // Can't meaningfully test os.Exit(0) in unit tests as it terminates the process + // This serves as a marker that the function exists and is pure side-effect + t.Run("exists", func(t *testing.T) { + // Verify function exists (compile-time check via reflect) + if reflect.ValueOf(Quit).IsNil() { + t.Error("Quit function is nil") + } + }) +} + +func TestRadioMenu_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast RadioMenu unit test") + + tests := []struct { + name string + stations []stationRecord + wantMenu bool + }{ + { + name: "empty_stations", + stations: []stationRecord{}, + wantMenu: true, + }, + { + name: "single_station", + stations: []stationRecord{ + {Name: "Test Station", Codec: "MP3", Bitrate: "128", Url: "http://test.com"}, + }, + wantMenu: true, + }, + { + name: "multiple_stations", + stations: []stationRecord{ + {Name: "Station One Very Long Name That Will Be Truncated", Codec: "AAC", Bitrate: "256", Url: "http://one.com"}, + {Name: "Station Two", Codec: "MP3", Bitrate: "128", Url: "http://two.com"}, + }, + wantMenu: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + menu := RadioMenu(tt.stations) + if menu == nil { + if tt.wantMenu { + t.Errorf("RadioMenu(%v) returned nil, want non-nil menu", tt.stations) + } + return + } + if !tt.wantMenu { + t.Errorf("RadioMenu(%v) returned non-nil menu, want nil", tt.stations) + } + }) + } +} diff --git a/stations_test.go b/stations_test.go new file mode 100644 index 0000000..e04e308 --- /dev/null +++ b/stations_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "bytes" + "os" + "testing" +) + +func TestShowVersion_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast showVersion unit test") + + tests := []struct { + name string + version string + expected string + }{ + {"default version", "1.0.0", "1.0.0\n"}, + {"empty version", "", "\n"}, + {"dev version", "dev", "dev\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalVersion := version + version = tt.version + defer func() { version = originalVersion }() + + // Safe stdout capture using pipe (no os.Stdout reassignment) + origStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + showVersion() + + w.Close() + os.Stdout = origStdout + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + got := buf.String() + + if got != tt.expected { + t.Errorf("showVersion() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestPrecheck_Unit(t *testing.T) { + t.Parallel() + t.Log("βœ“ Fast precheck unit test") + + // Simple smoke test β€” calls the real precheck() in the common "player installed" path + // (no mocking of globals β€” that's not allowed in Go) + precheck() + t.Log("βœ“ precheck ran without panic (happy path)") +} + +func TestPrecheck_Live(t *testing.T) { + if !testing.Short() { + t.Skip("skipping live integration test. Run with:\n go test -run TestPrecheck_Live -short -v") + } + t.Log("πŸ§ͺ Running live precheck integration test...") + + // Real precheck with whatever player is configured on this system + precheck() + t.Log("βœ“ Live precheck passed (no early exit)") +}