Files
minstrel/internal/library/fingerprint_settings_test.go
T
bvandeusenandClaude Opus 5.5 7e6228c13f
release / govulncheck (push) Successful in 20s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m15s
release / android (push) Successful in 5m26s
release / Build signed APK (releases and dev) (push) Successful in 5m45s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
docs: library comments say what the scanner and resolver do, not how they came to be (#6277)
Rewrites the internal/library comment blocks to rule 32. Task, milestone,
decision and rule ids are gone; the incidents behind them are restated as
the constraint they left (why a file is deleted before its row, why the
missing mark is a mark, why duplicates block on duration). Counts and
album names from one library are dropped from production comments; test
comments keep the names their fixtures use. The tagReadVersion log keeps
each version's before and after, without the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:27:34 -04:00

149 lines
6.3 KiB
Go

package library
import (
"context"
"errors"
"math"
"testing"
"time"
)
func TestValidateFingerprintSettings(t *testing.T) {
with := func(edit func(*FingerprintSettings)) FingerprintSettings {
s := DefaultFingerprintSettings
edit(&s)
return s
}
valid := map[string]FingerprintSettings{
"defaults": DefaultFingerprintSettings,
"shortest length": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = minChromaprintLengthSec }),
"longest length": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = maxChromaprintLengthSec }),
"strictest match": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = minAcousticMaxBitErrorRate }),
"loosest match": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = maxAcousticMaxBitErrorRate }),
"fewest at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = minBackfillConcurrency }),
"most at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = maxBackfillConcurrency }),
"shortest interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = minSweepIntervalHours }),
"longest interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = maxSweepIntervalHours }),
"switched off": with(func(s *FingerprintSettings) { s.Enabled = false }),
}
for name, s := range valid {
if err := validateFingerprintSettings(s); err != nil {
t.Errorf("%s: rejected: %v", name, err)
}
}
invalid := map[string]FingerprintSettings{
"length too short": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = minChromaprintLengthSec - 1 }),
"length too long": with(func(s *FingerprintSettings) { s.ChromaprintLengthSec = maxChromaprintLengthSec + 1 }),
"match too strict": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = minAcousticMaxBitErrorRate - 0.001 }),
"match too loose": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = maxAcousticMaxBitErrorRate + 0.001 }),
"match not a number": with(func(s *FingerprintSettings) { s.AcousticMaxBitErrorRate = math.NaN() }),
"none at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = 0 }),
"too many at once": with(func(s *FingerprintSettings) { s.BackfillConcurrency = maxBackfillConcurrency + 1 }),
"no interval": with(func(s *FingerprintSettings) { s.SweepIntervalHours = 0 }),
"interval too long": with(func(s *FingerprintSettings) { s.SweepIntervalHours = maxSweepIntervalHours + 1 }),
}
for name, s := range invalid {
if err := validateFingerprintSettings(s); !errors.Is(err, ErrFingerprintSettingOutOfRange) {
t.Errorf("%s: err = %v, want ErrFingerprintSettingOutOfRange", name, err)
}
}
}
func TestFingerprintSettingsService_NilServesDefaults(t *testing.T) {
var s *FingerprintSettingsService
if got := s.Get(); got != DefaultFingerprintSettings {
t.Fatalf("nil service Get = %+v, want the defaults", got)
}
// Validation still runs first, so a bad value is named rather than hidden
// behind the missing service.
bad := DefaultFingerprintSettings
bad.BackfillConcurrency = 0
if _, err := s.Set(context.Background(), bad); !errors.Is(err, ErrFingerprintSettingOutOfRange) {
t.Fatalf("nil service Set of a bad value: err = %v, want ErrFingerprintSettingOutOfRange", err)
}
}
func TestFingerprintSettingsService_Integration(t *testing.T) {
pool := newPool(t)
ctx := context.Background()
reload := func(step string) FingerprintSettings {
t.Helper()
fresh, err := NewFingerprintSettingsService(ctx, pool)
if err != nil {
t.Fatalf("%s: load: %v", step, err)
}
return fresh.Get()
}
withoutTime := func(s FingerprintSettings) FingerprintSettings {
s.UpdatedAt = time.Time{}
return s
}
svc, err := NewFingerprintSettingsService(ctx, pool)
if err != nil {
t.Fatalf("load: %v", err)
}
// ResetDB puts every column back to its migration default, so this pins the
// Go defaults to migration 0061's. Were they to drift, a database that
// could not be read would fingerprint differently from one that could.
loaded := svc.Get()
if loaded.UpdatedAt.IsZero() {
t.Fatal("loaded settings carry no updated_at")
}
if withoutTime(loaded) != DefaultFingerprintSettings {
t.Fatalf("stored defaults = %+v, want the Go defaults %+v", withoutTime(loaded), DefaultFingerprintSettings)
}
// Every bound the service accepts, the table accepts too. A CHECK tighter
// than validate would turn a value the card allows into a 500.
lowest := FingerprintSettings{
Enabled: false,
ChromaprintLengthSec: minChromaprintLengthSec,
AcousticMaxBitErrorRate: minAcousticMaxBitErrorRate,
BackfillConcurrency: minBackfillConcurrency,
SweepIntervalHours: minSweepIntervalHours,
AutoResolve: false,
}
highest := FingerprintSettings{
Enabled: true,
ChromaprintLengthSec: maxChromaprintLengthSec,
AcousticMaxBitErrorRate: maxAcousticMaxBitErrorRate,
BackfillConcurrency: maxBackfillConcurrency,
SweepIntervalHours: maxSweepIntervalHours,
AutoResolve: true,
}
for _, step := range []struct {
name string
want FingerprintSettings
}{{"lowest", lowest}, {"highest", highest}} {
saved, err := svc.Set(ctx, step.want)
if err != nil {
t.Fatalf("%s: save: %v", step.name, err)
}
// A save must move updated_at forward: it is what makes a sweep due.
if !saved.UpdatedAt.After(loaded.UpdatedAt) {
t.Fatalf("%s: updated_at %v did not move past %v", step.name, saved.UpdatedAt, loaded.UpdatedAt)
}
if withoutTime(saved) != step.want || withoutTime(svc.Get()) != step.want {
t.Fatalf("%s: saved %+v, cached %+v, want %+v", step.name, saved, svc.Get(), step.want)
}
if got := withoutTime(reload(step.name)); got != step.want {
t.Fatalf("%s: table holds %+v, want %+v", step.name, got, step.want)
}
}
// An out-of-range save changes nothing, in the cache or the table.
before := svc.Get()
bad := highest
bad.ChromaprintLengthSec = maxChromaprintLengthSec + 1
if _, err := svc.Set(ctx, bad); !errors.Is(err, ErrFingerprintSettingOutOfRange) {
t.Fatalf("out-of-range save: err = %v, want ErrFingerprintSettingOutOfRange", err)
}
if svc.Get() != before {
t.Fatalf("out-of-range save changed the cache to %+v", svc.Get())
}
if got := reload("after out-of-range save"); got != before {
t.Fatalf("out-of-range save changed the table to %+v", got)
}
}