ci / api / api (push) Successful in 56s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m23s
ci / db / db (push) Successful in 1m8s
ci / recommender / recommender (push) Failing after 40s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m25s
ci / ts / ts (packages + web) (push) Successful in 7m20s
161 lines
6.2 KiB
Go
161 lines
6.2 KiB
Go
//go:build integration
|
|
|
|
package dbtest
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"pipu/api/internal/library"
|
|
"pipu/api/internal/platform/db"
|
|
)
|
|
|
|
func TestLibraryDiagnostics(t *testing.T) {
|
|
pool := connect(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, db.Migrate(ctx, pool))
|
|
store := library.NewStore(pool)
|
|
src, err := store.SaveSource(ctx, library.Source{ID: uuid.New(), Name: "diagnostics", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, NFSOptions: "ro"})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, store.DeleteSource(ctx, src.ID))
|
|
require.NoError(t, store.CleanupOrphans(ctx))
|
|
})
|
|
before, err := store.Counts(ctx)
|
|
require.NoError(t, err)
|
|
badID, err := store.SaveFile(ctx, src.ID, uuid.Nil, library.FileData{RelPath: "broken.mp3", Mtime: time.Now(), ScanError: "invalid audio"}, nil)
|
|
require.NoError(t, err)
|
|
counts, err := store.Counts(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, before.Errors+1, counts.Errors)
|
|
require.NoError(t, store.MarkMissing(ctx, []uuid.UUID{badID}, time.Now()))
|
|
counts, err = store.Counts(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, before.Errors, counts.Errors)
|
|
fileErrors, err := store.FileErrors(ctx, "", 200)
|
|
require.NoError(t, err)
|
|
for _, item := range fileErrors {
|
|
require.NotEqual(t, badID, item.ID, "absent files must not remain in read errors")
|
|
}
|
|
// Failed analysis jobs retain their place in diagnostics even without a file.
|
|
root := t.TempDir()
|
|
scanner := library.NewScanner(store, fakeProber{}, library.NewProgressBoard(), slog.New(slog.DiscardHandler))
|
|
tracks := map[uuid.UUID]bool{}
|
|
for _, name := range []string{"Artist - First.flac", "Artist - Second.flac"} {
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, name), []byte(name), 0o644))
|
|
fileID, err := scanner.ScanFile(ctx, src, root, name)
|
|
require.NoError(t, err)
|
|
var trackID uuid.UUID
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT track_id FROM media_files WHERE id = $1`, fileID).Scan(&trackID))
|
|
tracks[trackID] = true
|
|
_, err = pool.Exec(ctx, `UPDATE analysis_jobs SET status = 'failed', error = 'audio is shorter than 1 s' WHERE track_id = $1`, trackID)
|
|
require.NoError(t, err)
|
|
if name == "Artist - Second.flac" {
|
|
_, err = pool.Exec(ctx, `DELETE FROM media_files WHERE id = $1`, fileID)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
var paths, missing int
|
|
after := uuid.Nil
|
|
for {
|
|
page, err := store.AnalysisErrors(ctx, after, 1)
|
|
require.NoError(t, err)
|
|
if len(page) == 0 {
|
|
break
|
|
}
|
|
item := page[0]
|
|
require.NotEqual(t, after, item.TrackID, "pagination must advance")
|
|
after = item.TrackID
|
|
if !tracks[item.TrackID] {
|
|
continue
|
|
}
|
|
require.Equal(t, "audio is shorter than 1 s", item.Error)
|
|
if item.RelPath == nil {
|
|
missing++
|
|
} else {
|
|
paths++
|
|
require.Equal(t, "Artist - First.flac", *item.RelPath)
|
|
require.Equal(t, src.ID, *item.SourceID)
|
|
}
|
|
}
|
|
require.Equal(t, 1, paths)
|
|
require.Equal(t, 1, missing)
|
|
}
|
|
|
|
func TestScanExcludedFolders(t *testing.T) {
|
|
pool := connect(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, db.Migrate(ctx, pool))
|
|
store := library.NewStore(pool)
|
|
src, err := store.SaveSource(ctx, library.Source{ID: uuid.New(), Name: "exclusions", Type: library.SourceLocal, Status: "mounted", TagsFirst: true, NFSOptions: "ro"})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, store.DeleteSource(ctx, src.ID))
|
|
require.NoError(t, store.CleanupOrphans(ctx))
|
|
})
|
|
root := t.TempDir()
|
|
for _, rel := range []string{"Downloads_PumpEars/Artist - Loop.wav", "Downloads_PumpEars/nested/Artist - Hit.wav", "Downloads_PumpEars-copy/Artist - Song.flac"} {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
|
|
require.NoError(t, os.WriteFile(p, []byte(rel), 0o644))
|
|
}
|
|
board := library.NewProgressBoard()
|
|
scanner := library.NewScanner(store, fakeProber{}, board, slog.New(slog.DiscardHandler))
|
|
scan := func() library.Progress {
|
|
require.NoError(t, scanner.Start(src, root, true))
|
|
require.Eventually(t, func() bool { p, ok := board.Get(src.ID); return ok && !p.Running }, 10*time.Second, 10*time.Millisecond)
|
|
p, _ := board.Get(src.ID)
|
|
require.Empty(t, p.Error)
|
|
return p
|
|
}
|
|
require.Equal(t, 3, scan().Found)
|
|
_, err = pool.Exec(ctx, `UPDATE analysis_jobs SET status = 'failed', error = 'too short' WHERE track_id IN (SELECT track_id FROM media_files WHERE source_id = $1)`, src.ID)
|
|
require.NoError(t, err)
|
|
before, err := store.AnalysisCounts(ctx)
|
|
require.NoError(t, err)
|
|
src.ExcludedDirs = []string{"Downloads_PumpEars"}
|
|
src, err = store.SaveSource(ctx, src)
|
|
require.NoError(t, err)
|
|
saved, err := store.GetSource(ctx, src.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, src.ExcludedDirs, saved.ExcludedDirs)
|
|
counts, err := store.AnalysisCounts(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, before.Failed-2, counts.Failed, "old failed jobs from excluded folders disappear immediately")
|
|
_, err = store.RetryFailedAnalysis(ctx)
|
|
require.NoError(t, err)
|
|
var failed int
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT count(*) FROM analysis_jobs WHERE status = 'failed' AND track_id IN (SELECT track_id FROM media_files WHERE source_id = $1)`, src.ID).Scan(&failed))
|
|
require.Equal(t, 2, failed, "retry must not requeue excluded jobs")
|
|
p := scan()
|
|
require.Equal(t, 1, p.Found)
|
|
require.Equal(t, 1, p.ToProcess)
|
|
require.Zero(t, p.Missing)
|
|
files, err := store.Files(ctx, src.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, files, 3, "excluded records must be preserved")
|
|
for _, f := range files {
|
|
require.False(t, f.Missing, "excluded files must not be treated as absent")
|
|
}
|
|
// Literal matching must not treat % and _ as SQL wildcards.
|
|
for _, test := range []struct {
|
|
path string
|
|
excluded bool
|
|
}{{"100%_loops/a.wav", true}, {"100xxloops/a.wav", false}, {"100%_loops-copy/a.wav", false}} {
|
|
var excluded bool
|
|
require.NoError(t, pool.QueryRow(ctx, `SELECT library_path_excluded($1, ARRAY['100%_loops'])`, test.path).Scan(&excluded))
|
|
require.Equal(t, test.excluded, excluded)
|
|
}
|
|
src.ExcludedDirs = []string{}
|
|
src, err = store.SaveSource(ctx, src)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, scan().Found, "clearing exclusions makes the subtree eligible again")
|
|
}
|