diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-03 19:50:57 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-03 19:50:57 +0300 |
| commit | df8f3713abee8b5ec53b5751032200b53673e285 (patch) | |
| tree | 58b3909e345b02edfb5f5613081f1bd4fde66571 /cmd | |
| parent | 30c2b0fe8232cc748ab2bded6ab4d76febe32425 (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.go | 23 | ||||
| -rw-r--r-- | cmd/mediaplayer/main_test.go | 137 |
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") + } +} |
