mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-02 08:27:56 +01:00
test(scanner): improve handling of filesystem errors and ensure warnings are returned
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
4c06cd2ac8
commit
9e90145291
1 changed files with 326 additions and 4 deletions
|
|
@ -2,12 +2,15 @@ package scanner_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
|
|
@ -256,16 +259,17 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
|
|||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rockLib.TotalSongs).To(Equal(4))
|
||||
Expect(rockLib.TotalAlbums).To(Equal(2))
|
||||
Expect(rockLib.TotalArtists).To(BeNumerically(">=", 2))
|
||||
Expect(rockLib.TotalFolders).To(BeNumerically(">=", 2))
|
||||
|
||||
Expect(rockLib.TotalArtists).To(Equal(3)) // The Beatles, Led Zeppelin, [Unknown Artist]
|
||||
Expect(rockLib.TotalFolders).To(Equal(2)) // Abbey Road, IV (only folders with audio files)
|
||||
|
||||
// Check Jazz library stats
|
||||
jazzLib, err := ds.Library(ctx).Get(lib2.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzLib.TotalSongs).To(Equal(4))
|
||||
Expect(jazzLib.TotalAlbums).To(Equal(2))
|
||||
Expect(jazzLib.TotalArtists).To(BeNumerically(">=", 2))
|
||||
Expect(jazzLib.TotalFolders).To(BeNumerically(">=", 2))
|
||||
Expect(jazzLib.TotalArtists).To(Equal(3)) // Miles Davis, John Coltrane, [Unknown Artist]
|
||||
Expect(jazzLib.TotalFolders).To(Equal(2)) // Kind of Blue, Giant Steps (only folders with audio files)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -506,4 +510,322 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
|
|||
Expect(presentRockFiles[0].Title).To(Equal("Hells Bells"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Error Handling - Multi-Library", func() {
|
||||
Context("Filesystem errors affecting one library", func() {
|
||||
var rockFS storagetest.FakeFS
|
||||
|
||||
BeforeEach(func() {
|
||||
// Set up content for both libraries
|
||||
rock := template(_t{"albumartist": "AC/DC", "album": "Back in Black", "year": 1980, "genre": "Rock"})
|
||||
jazz := template(_t{"albumartist": "Miles Davis", "album": "Kind of Blue", "year": 1959, "genre": "Jazz"})
|
||||
|
||||
rockFS = createFS("rock", fstest.MapFS{
|
||||
"AC-DC/Back in Black/01 - Hells Bells.mp3": rock(track(1, "Hells Bells")),
|
||||
"AC-DC/Back in Black/02 - Shoot to Thrill.mp3": rock(track(2, "Shoot to Thrill")),
|
||||
})
|
||||
|
||||
createFS("jazz", fstest.MapFS{
|
||||
"Miles Davis/Kind of Blue/01 - So What.mp3": jazz(track(1, "So What")),
|
||||
"Miles Davis/Kind of Blue/02 - Freddie Freeloader.mp3": jazz(track(2, "Freddie Freeloader")),
|
||||
})
|
||||
})
|
||||
|
||||
It("should not affect scanning of other libraries", func() {
|
||||
// Inject filesystem read error in rock library only
|
||||
rockFS.SetError("AC-DC/Back in Black/01 - Hells Bells.mp3", errors.New("filesystem read error"))
|
||||
|
||||
// Scan should succeed overall and return warnings
|
||||
warnings, err := s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for filesystem errors")
|
||||
|
||||
// Jazz library should have been scanned successfully
|
||||
jazzFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzFiles).To(HaveLen(2))
|
||||
Expect(jazzFiles[0].Title).To(BeElementOf("So What", "Freddie Freeloader"))
|
||||
Expect(jazzFiles[1].Title).To(BeElementOf("So What", "Freddie Freeloader"))
|
||||
|
||||
// Rock library may have partial content (depending on scanner implementation)
|
||||
rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib1.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// No specific expectation - some files may have been imported despite errors
|
||||
_ = rockFiles
|
||||
|
||||
// Verify jazz library stats are correct
|
||||
jazzLib, err := ds.Library(ctx).Get(lib2.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzLib.TotalSongs).To(Equal(2))
|
||||
|
||||
// Error should be empty (warnings don't count as scan errors)
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should continue with warnings for affected library", func() {
|
||||
// Inject read errors on multiple files in rock library
|
||||
rockFS.SetError("AC-DC/Back in Black/01 - Hells Bells.mp3", errors.New("read error 1"))
|
||||
rockFS.SetError("AC-DC/Back in Black/02 - Shoot to Thrill.mp3", errors.New("read error 2"))
|
||||
|
||||
// Scan should complete with warnings for multiple filesystem errors
|
||||
warnings, err := s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for multiple filesystem errors")
|
||||
|
||||
// Jazz library should be completely unaffected
|
||||
jazzFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzFiles).To(HaveLen(2))
|
||||
|
||||
// Jazz library statistics should be accurate
|
||||
jazzLib, err := ds.Library(ctx).Get(lib2.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzLib.TotalSongs).To(Equal(2))
|
||||
Expect(jazzLib.TotalAlbums).To(Equal(1))
|
||||
|
||||
// Error should be empty (warnings don't count as scan errors)
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("Database errors during multi-library scanning", func() {
|
||||
BeforeEach(func() {
|
||||
// Set up content for both libraries
|
||||
rock := template(_t{"albumartist": "Queen", "album": "News of the World", "year": 1977, "genre": "Rock"})
|
||||
jazz := template(_t{"albumartist": "Bill Evans", "album": "Waltz for Debby", "year": 1961, "genre": "Jazz"})
|
||||
|
||||
createFS("rock", fstest.MapFS{
|
||||
"Queen/News of the World/01 - We Will Rock You.mp3": rock(track(1, "We Will Rock You")),
|
||||
})
|
||||
|
||||
createFS("jazz", fstest.MapFS{
|
||||
"Bill Evans/Waltz for Debby/01 - My Foolish Heart.mp3": jazz(track(1, "My Foolish Heart")),
|
||||
})
|
||||
})
|
||||
|
||||
It("should propagate database errors and stop scanning", func() {
|
||||
// Install mock repo that injects DB error
|
||||
mfRepo := &mockMediaFileRepo{
|
||||
MediaFileRepository: ds.RealDS.MediaFile(ctx),
|
||||
GetMissingAndMatchingError: errors.New("database connection failed"),
|
||||
}
|
||||
ds.MockedMediaFile = mfRepo
|
||||
|
||||
// Scan should return the database error
|
||||
Expect(runScanner(ctx, false)).To(MatchError(ContainSubstring("database connection failed")))
|
||||
|
||||
// Error should be recorded in scanner properties
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(ContainSubstring("database connection failed"))
|
||||
})
|
||||
|
||||
It("should preserve error information in scanner properties", func() {
|
||||
// Install mock repo that injects DB error
|
||||
mfRepo := &mockMediaFileRepo{
|
||||
MediaFileRepository: ds.RealDS.MediaFile(ctx),
|
||||
GetMissingAndMatchingError: errors.New("critical database error"),
|
||||
}
|
||||
ds.MockedMediaFile = mfRepo
|
||||
|
||||
// Attempt scan (should fail)
|
||||
Expect(runScanner(ctx, false)).To(HaveOccurred())
|
||||
|
||||
// Check that error is recorded in scanner properties
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(ContainSubstring("critical database error"))
|
||||
|
||||
// Scan type should still be recorded
|
||||
scanType, _ := ds.Property(ctx).DefaultGet(consts.LastScanTypeKey, "")
|
||||
Expect(scanType).To(BeElementOf("incremental", "quick"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("Mixed error scenarios", func() {
|
||||
var rockFS storagetest.FakeFS
|
||||
|
||||
BeforeEach(func() {
|
||||
// Set up rock library with filesystem that can error
|
||||
rock := template(_t{"albumartist": "Metallica", "album": "Master of Puppets", "year": 1986, "genre": "Metal"})
|
||||
rockFS = createFS("rock", fstest.MapFS{
|
||||
"Metallica/Master of Puppets/01 - Battery.mp3": rock(track(1, "Battery")),
|
||||
"Metallica/Master of Puppets/02 - Master of Puppets.mp3": rock(track(2, "Master of Puppets")),
|
||||
})
|
||||
|
||||
// Set up jazz library normally
|
||||
jazz := template(_t{"albumartist": "Herbie Hancock", "album": "Head Hunters", "year": 1973, "genre": "Jazz"})
|
||||
createFS("jazz", fstest.MapFS{
|
||||
"Herbie Hancock/Head Hunters/01 - Chameleon.mp3": jazz(track(1, "Chameleon")),
|
||||
})
|
||||
})
|
||||
|
||||
It("should handle filesystem errors in one library while other succeeds", func() {
|
||||
// Inject filesystem error in rock library
|
||||
rockFS.SetError("Metallica/Master of Puppets/01 - Battery.mp3", errors.New("disk read error"))
|
||||
|
||||
// Scan should complete with warnings (not hard error)
|
||||
warnings, err := s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for filesystem error")
|
||||
|
||||
// Jazz library should scan completely successfully
|
||||
jazzFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzFiles).To(HaveLen(1))
|
||||
Expect(jazzFiles[0].Title).To(Equal("Chameleon"))
|
||||
|
||||
// Jazz library statistics should be accurate
|
||||
jazzLib, err := ds.Library(ctx).Get(lib2.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzLib.TotalSongs).To(Equal(1))
|
||||
Expect(jazzLib.TotalAlbums).To(Equal(1))
|
||||
|
||||
// Rock library may have partial content (depending on scanner implementation)
|
||||
rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib1.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// No specific expectation - some files may have been imported despite errors
|
||||
_ = rockFiles
|
||||
|
||||
// Error should be empty (warnings don't count as scan errors)
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("should handle partial failures gracefully", func() {
|
||||
// Create a scenario where rock has filesystem issues and jazz has normal content
|
||||
rockFS.SetError("Metallica/Master of Puppets/01 - Battery.mp3", errors.New("file corruption"))
|
||||
|
||||
// Do an initial scan with filesystem error
|
||||
warnings, err := s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for file corruption")
|
||||
|
||||
// Verify that the working parts completed successfully
|
||||
jazzFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzFiles).To(HaveLen(1))
|
||||
|
||||
// Scanner properties should reflect successful completion despite warnings
|
||||
scanType, _ := ds.Property(ctx).DefaultGet(consts.LastScanTypeKey, "")
|
||||
Expect(scanType).To(Equal("full"))
|
||||
|
||||
// Start time should be recorded
|
||||
startTimeStr, _ := ds.Property(ctx).DefaultGet(consts.LastScanStartTimeKey, "")
|
||||
Expect(startTimeStr).ToNot(BeEmpty())
|
||||
|
||||
// Error should be empty (warnings don't count as scan errors)
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("Error recovery in multi-library context", func() {
|
||||
It("should recover from previous library-specific errors", func() {
|
||||
// Set up initial content
|
||||
rock := template(_t{"albumartist": "Iron Maiden", "album": "The Number of the Beast", "year": 1982, "genre": "Metal"})
|
||||
jazz := template(_t{"albumartist": "John Coltrane", "album": "Giant Steps", "year": 1960, "genre": "Jazz"})
|
||||
|
||||
rockFS := createFS("rock", fstest.MapFS{
|
||||
"Iron Maiden/The Number of the Beast/01 - Invaders.mp3": rock(track(1, "Invaders")),
|
||||
})
|
||||
|
||||
createFS("jazz", fstest.MapFS{
|
||||
"John Coltrane/Giant Steps/01 - Giant Steps.mp3": jazz(track(1, "Giant Steps")),
|
||||
})
|
||||
|
||||
// First scan with filesystem error in rock
|
||||
rockFS.SetError("Iron Maiden/The Number of the Beast/01 - Invaders.mp3", errors.New("temporary disk error"))
|
||||
warnings, err := s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred()) // Should succeed with warnings
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for temporary disk error")
|
||||
|
||||
// Clear the error and add more content - recreate the filesystem completely
|
||||
rockFS.ClearError("Iron Maiden/The Number of the Beast/01 - Invaders.mp3")
|
||||
|
||||
// Create a new filesystem with both files
|
||||
createFS("rock", fstest.MapFS{
|
||||
"Iron Maiden/The Number of the Beast/01 - Invaders.mp3": rock(track(1, "Invaders")),
|
||||
"Iron Maiden/The Number of the Beast/02 - Children of the Damned.mp3": rock(track(2, "Children of the Damned")),
|
||||
})
|
||||
|
||||
// Second scan should recover and import all rock content
|
||||
warnings, err = s.ScanAll(ctx, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(warnings).ToNot(BeEmpty(), "Should have warnings for temporary disk error")
|
||||
|
||||
// Verify both libraries now have content (at least jazz should work)
|
||||
rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib1.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// The scanner should recover and import both rock files
|
||||
Expect(len(rockFiles)).To(Equal(2))
|
||||
|
||||
jazzFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||
Filters: squirrel.Eq{"library_id": lib2.ID},
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzFiles).To(HaveLen(1))
|
||||
|
||||
// Both libraries should have correct content counts
|
||||
rockLib, err := ds.Library(ctx).Get(lib1.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(rockLib.TotalSongs).To(Equal(2))
|
||||
|
||||
jazzLib, err := ds.Library(ctx).Get(lib2.ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(jazzLib.TotalSongs).To(Equal(1))
|
||||
|
||||
// Error should be empty (successful recovery)
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("Scanner Properties", func() {
|
||||
It("should persist last scan type, start time and error properties", func() {
|
||||
// trivial FS setup
|
||||
rock := template(_t{"albumartist": "AC/DC", "album": "Back in Black", "year": 1980, "genre": "Rock"})
|
||||
_ = createFS("rock", fstest.MapFS{
|
||||
"AC-DC/Back in Black/01 - Hells Bells.mp3": rock(track(1, "Hells Bells")),
|
||||
})
|
||||
|
||||
// Run a full scan
|
||||
Expect(runScanner(ctx, true)).To(Succeed())
|
||||
|
||||
// Validate properties
|
||||
scanType, _ := ds.Property(ctx).DefaultGet(consts.LastScanTypeKey, "")
|
||||
Expect(scanType).To(Equal("full"))
|
||||
|
||||
startTimeStr, _ := ds.Property(ctx).DefaultGet(consts.LastScanStartTimeKey, "")
|
||||
Expect(startTimeStr).ToNot(BeEmpty())
|
||||
_, err := time.Parse(time.RFC3339, startTimeStr)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
lastError, err := ds.Property(ctx).DefaultGet(consts.LastScanErrorKey, "unset")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lastError).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue