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
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>
149 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|