Files

99 lines
3.1 KiB
Go
Raw Permalink Normal View History

package scanner
import (
"errors"
"fmt"
"strings"
"sync"
"testing"
)
func TestScanFailuresJoinsEverythingUnderTheCap(t *testing.T) {
var failures scanFailures
sentinel := errors.New("boom")
failures.addf("/books/a: %w", sentinel)
failures.addf("/books/b: %w", sentinel)
if got := failures.len(); got != 2 {
t.Fatalf("len() = %d, want 2", got)
}
joined := failures.join()
if !errors.Is(joined, sentinel) {
t.Fatalf("joined error = %v, want it to wrap the sentinel", joined)
}
for _, want := range []string{"/books/a", "/books/b"} {
if !strings.Contains(joined.Error(), want) {
t.Fatalf("joined error %q missing %q", joined, want)
}
}
if strings.Contains(joined.Error(), "elided") {
t.Fatalf("joined error %q reported elisions with nothing elided", joined)
}
}
// A misconfigured ffprobe fails every candidate in the library, so the
// all-failed summary must stay a bounded diagnostic instead of growing with
// the folder count: it is written verbatim into scan_runs.error_message and
// republished to every admin SSE subscriber.
func TestScanFailuresCapsRetainedErrorsAndReportsTheRemainder(t *testing.T) {
var failures scanFailures
const total = maxRetainedScanFailures + 500
for i := 0; i < total; i++ {
failures.addf("/books/%d: %w", i, errors.New("fork/exec /usr/lib/jellyfin-ffmpeg/ffprobe: no such file or directory"))
}
if got := failures.len(); got != total {
t.Fatalf("len() = %d, want %d — the count must include elided failures", got, total)
}
if got := len(failures.retained); got != maxRetainedScanFailures {
t.Fatalf("retained %d errors, want the cap of %d", got, maxRetainedScanFailures)
}
joined := failures.join()
if !strings.Contains(joined.Error(), fmt.Sprintf("and %d more failures (elided)", total-maxRetainedScanFailures)) {
t.Fatalf("joined error does not report the elided remainder: %q", joined)
}
// The retained sample still names the cause, which is the whole point of
// the summary.
if !strings.Contains(joined.Error(), "/books/0") {
t.Fatalf("joined error dropped the first failure: %q", joined)
}
if strings.Contains(joined.Error(), fmt.Sprintf("/books/%d:", total-1)) {
t.Fatalf("joined error retained a failure past the cap: %q", joined)
}
}
func TestScanFailuresJoinIsNilWhenNothingFailed(t *testing.T) {
var failures scanFailures
if got := failures.len(); got != 0 {
t.Fatalf("len() = %d, want 0", got)
}
if err := failures.join(); err != nil {
t.Fatalf("join() = %v, want nil", err)
}
}
// The audiobook, ebook, and manga scans record failures from a worker pool.
func TestScanFailuresIsConcurrencySafe(t *testing.T) {
var failures scanFailures
var wg sync.WaitGroup
const workers, perWorker = 8, 200
for i := 0; i < workers; i++ {
wg.Add(1)
go func(worker int) {
defer wg.Done()
for j := 0; j < perWorker; j++ {
failures.addf("/books/%d-%d: %w", worker, j, errors.New("probe failed"))
}
}(i)
}
wg.Wait()
if got := failures.len(); got != workers*perWorker {
t.Fatalf("len() = %d, want %d", got, workers*perWorker)
}
if got := len(failures.retained); got != maxRetainedScanFailures {
t.Fatalf("retained %d errors, want the cap of %d", got, maxRetainedScanFailures)
}
}