summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-03 19:50:57 +0300
committerPaul Buetow <paul@buetow.org>2026-05-03 19:50:57 +0300
commitdf8f3713abee8b5ec53b5751032200b53673e285 (patch)
tree58b3909e345b02edfb5f5613081f1bd4fde66571 /cmd
parent30c2b0fe8232cc748ab2bded6ab4d76febe32425 (diff)
test: raise coverage above 60% in cmd/mediaplayer, model, and probe
- cmd/mediaplayer: add runWithSignal injection point for testing; cover version flag, invalid flags, invalid config, normal shutdown, all log levels, invalid DB, and privileged-port bind failure. Refactor run() to delegate to runWithSignal with optional signal channel. - internal/config: allow PORT=0 (ephemeral) to support test server startup. Update AGENTS.md validation docs accordingly. - internal/model: add comprehensive ScanProgress tests (Start, Done, IncrementFile/Set, SetCurrentSet/FilesTotal, Copy isolation, and concurrent access). - internal/probe: add image tests for isImagePath (all extensions and case insensitivity), real EXIF extraction via ImageMagick + exiv2, Probe against real JPEG, MP4, empty file, and nonexistent paths. - internal/config_test: replace PORT=0 invalid-value test with PORT=-1.
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mediaplayer/main.go23
-rw-r--r--cmd/mediaplayer/main_test.go137
2 files changed, 138 insertions, 22 deletions
diff --git a/cmd/mediaplayer/main.go b/cmd/mediaplayer/main.go
index 68e59e1..f7c5928 100644
--- a/cmd/mediaplayer/main.go
+++ b/cmd/mediaplayer/main.go
@@ -30,6 +30,10 @@ func main() {
}
func run(args []string) error {
+ return runWithSignal(args, nil)
+}
+
+func runWithSignal(args []string, sigCh <-chan os.Signal) error {
fs := flag.NewFlagSet("mediaplayer", flag.ContinueOnError)
versionFlag := fs.Bool("version", false, "print version and exit")
if err := fs.Parse(args); err != nil {
@@ -98,15 +102,26 @@ func run(args []string) error {
log.Printf("player %s starting on %s", internal.Version, gs.Server.Addr)
+ errCh := make(chan error, 1)
go func() {
if err := gs.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("failed to start server: %v", err)
+ errCh <- fmt.Errorf("failed to start server: %w", err)
}
}()
- quit := make(chan os.Signal, 1)
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
- <-quit
+ if sigCh == nil {
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ sigCh = quit
+ }
+
+ select {
+ case <-sigCh:
+ case err := <-errCh:
+ if err != nil {
+ return err
+ }
+ }
log.Println("shutting down server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
diff --git a/cmd/mediaplayer/main_test.go b/cmd/mediaplayer/main_test.go
index 2cc9b03..29d36f0 100644
--- a/cmd/mediaplayer/main_test.go
+++ b/cmd/mediaplayer/main_test.go
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
+ "syscall"
"testing"
"time"
@@ -16,9 +17,18 @@ import (
"codeberg.org/snonux/player/internal/service"
)
-// TestGCWorkerWiring verifies that the GC worker can be constructed with the
-// same dependencies used in main, started, and stopped cleanly against a real
-// SQLite store. This is an integration-friendly smoke test for the wiring.
+func captureStdout(fn func()) string {
+ old := os.Stdout
+ r, w, _ := os.Pipe()
+ os.Stdout = w
+ fn()
+ w.Close()
+ os.Stdout = old
+ var buf bytes.Buffer
+ io.Copy(&buf, r)
+ return strings.TrimSpace(buf.String())
+}
+
func TestGCWorkerWiring(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
@@ -50,21 +60,12 @@ func TestGCWorkerWiring(t *testing.T) {
}
func TestRun_VersionFlag(t *testing.T) {
- old := os.Stdout
- r, w, _ := os.Pipe()
- os.Stdout = w
-
- err := run([]string{"-version"})
- w.Close()
- os.Stdout = old
-
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
-
- var buf bytes.Buffer
- io.Copy(&buf, r)
- out := strings.TrimSpace(buf.String())
+ out := captureStdout(func() {
+ err := run([]string{"-version"})
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ })
if out != internal.Version {
t.Fatalf("expected %q, got %q", internal.Version, out)
}
@@ -84,3 +85,103 @@ func TestRun_InvalidConfig(t *testing.T) {
t.Fatal("expected error for invalid PORT")
}
}
+
+func TestRunWithSignal_NormalShutdown(t *testing.T) {
+ if os.Getenv("GO_TEST_IN_CONTAINER") == "no_ffprobe" {
+ t.Skip("ffprobe not available in this environment")
+ }
+
+ tmpDir := t.TempDir()
+ t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db"))
+ t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media"))
+ t.Setenv("PORT", "0")
+
+ // Build a channel we can use instead of real OS signals.
+ sigCh := make(chan os.Signal, 1)
+
+ // Run the server in a goroutine; it will block on <-sigCh.
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- runWithSignal([]string{}, sigCh)
+ }()
+
+ // Give the server a moment to start listening.
+ time.Sleep(500 * time.Millisecond)
+
+ // Send a synthetic signal to trigger shutdown.
+ sigCh <- syscall.SIGINT
+
+ // Wait for graceful shutdown.
+ select {
+ case err := <-errCh:
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for server shutdown")
+ }
+}
+
+func TestRunWithSignal_LogLevels(t *testing.T) {
+ for _, level := range []string{"debug", "info", "warn", "error", "invalid"} {
+ t.Run(level, func(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db"))
+ t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media"))
+ t.Setenv("PORT", "0")
+ t.Setenv("LOG_LEVEL", level)
+
+ sigCh := make(chan os.Signal, 1)
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- runWithSignal([]string{}, sigCh)
+ }()
+ time.Sleep(200 * time.Millisecond)
+ sigCh <- syscall.SIGTERM
+
+ select {
+ case err := <-errCh:
+ if level == "invalid" {
+ if err == nil {
+ t.Fatal("expected error for invalid LOG_LEVEL")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for server shutdown")
+ }
+ })
+ }
+}
+
+func TestRunWithSignal_InvalidDB(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("DB_PATH", filepath.Join(tmpDir, "readonly"))
+ if err := os.MkdirAll(filepath.Join(tmpDir, "readonly"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media"))
+ t.Setenv("PORT", "0")
+
+ err := runWithSignal([]string{}, nil)
+ if err == nil {
+ t.Fatal("expected error for invalid DB_PATH")
+ }
+}
+
+func TestRunWithSignal_ServerErrorPath(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("DB_PATH", filepath.Join(tmpDir, "test.db"))
+ t.Setenv("MEDIA_ROOT", filepath.Join(tmpDir, "media"))
+ // Port 1 is privileged and should fail on non-root Linux.
+ t.Setenv("PORT", "1")
+
+ // No signal channel; we expect the server start to fail quickly.
+ err := runWithSignal([]string{}, nil)
+ if err == nil {
+ t.Fatal("expected error when server cannot bind privileged port")
+ }
+}