mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
* fix(scanner): detect in-place playlist edits via the folder content hash Signed-off-by: junkerderprovinz <jdp@braethoria.com> * docs(scanner): clarify the playlist entries in the folder hash Shorten the comment on the playlist loop, and record why the playlist count stays in the hash header: it is redundant with the loop for change detection, but removing it changes the hashed byte stream for every folder, including folders without playlists, which would mark every folder outdated on the first scan after upgrade. Signed-off-by: junkerderprovinz <jdp@braethoria.com> * test(scanner): pin filename and size into the playlist hash assertions The playlist size test called time.Now() twice, so the modtime differed too and carried the assertion — dropping info.Size() from the hash left the suite green. It now shares one baseTime. A new rename test swaps the map key with count, size and modtime held constant, so dropping the filename from the hash fails. Both mutations were verified to fail before this change and pass after. --------- Signed-off-by: junkerderprovinz <jdp@braethoria.com> Co-authored-by: Deluan <deluan@navidrome.org>
608 lines
17 KiB
Go
608 lines
17 KiB
Go
package scanner
|
|
|
|
import (
|
|
"io/fs"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/model"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("folder_entry", func() {
|
|
var (
|
|
lib model.Library
|
|
job *scanJob
|
|
path string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
lib = model.Library{
|
|
ID: 500,
|
|
Path: "/music",
|
|
LastScanStartedAt: time.Now().Add(-1 * time.Hour),
|
|
FullScanInProgress: false,
|
|
}
|
|
job = &scanJob{
|
|
lib: lib,
|
|
lastUpdates: make(map[string]model.FolderUpdateInfo),
|
|
}
|
|
path = "test/folder"
|
|
})
|
|
|
|
Describe("newFolderEntry", func() {
|
|
It("creates a new folder entry with correct initialization", func() {
|
|
folderID := model.FolderID(lib, path)
|
|
updateInfo := model.FolderUpdateInfo{
|
|
UpdatedAt: time.Now().Add(-30 * time.Minute),
|
|
Hash: "previous-hash",
|
|
}
|
|
|
|
entry := newFolderEntry(job, folderID, path, updateInfo)
|
|
|
|
Expect(entry.id).To(Equal(folderID))
|
|
Expect(entry.job).To(Equal(job))
|
|
Expect(entry.path).To(Equal(path))
|
|
Expect(entry.audioFiles).To(BeEmpty())
|
|
Expect(entry.imageFiles).To(BeEmpty())
|
|
Expect(entry.playlistFiles).To(BeEmpty())
|
|
Expect(entry.albumIDMap).To(BeEmpty())
|
|
Expect(entry.updTime).To(Equal(updateInfo.UpdatedAt))
|
|
Expect(entry.prevHash).To(Equal(updateInfo.Hash))
|
|
})
|
|
})
|
|
|
|
Describe("createFolderEntry", func() {
|
|
It("removes the lastUpdate from the job after creation", func() {
|
|
folderID := model.FolderID(lib, path)
|
|
updateInfo := model.FolderUpdateInfo{
|
|
UpdatedAt: time.Now().Add(-30 * time.Minute),
|
|
Hash: "previous-hash",
|
|
}
|
|
job.lastUpdates[folderID] = updateInfo
|
|
|
|
entry := job.createFolderEntry(path)
|
|
|
|
Expect(entry.updTime).To(Equal(updateInfo.UpdatedAt))
|
|
Expect(entry.prevHash).To(Equal(updateInfo.Hash))
|
|
Expect(job.lastUpdates).ToNot(HaveKey(folderID))
|
|
})
|
|
})
|
|
|
|
Describe("folderEntry", func() {
|
|
var entry *folderEntry
|
|
|
|
BeforeEach(func() {
|
|
folderID := model.FolderID(lib, path)
|
|
entry = newFolderEntry(job, folderID, path, model.FolderUpdateInfo{})
|
|
})
|
|
|
|
Describe("hasNoFiles", func() {
|
|
It("returns true when folder has no files or subfolders", func() {
|
|
Expect(entry.hasNoFiles()).To(BeTrue())
|
|
})
|
|
|
|
It("returns false when folder has audio files", func() {
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{name: "test.mp3"}
|
|
Expect(entry.hasNoFiles()).To(BeFalse())
|
|
})
|
|
|
|
It("returns false when folder has image files", func() {
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{name: "cover.jpg"}
|
|
Expect(entry.hasNoFiles()).To(BeFalse())
|
|
})
|
|
|
|
It("returns false when folder has playlists", func() {
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{name: "list.m3u"}
|
|
Expect(entry.hasNoFiles()).To(BeFalse())
|
|
})
|
|
|
|
It("ignores subfolders when checking for no files", func() {
|
|
entry.numSubFolders = 1
|
|
Expect(entry.hasNoFiles()).To(BeTrue())
|
|
})
|
|
|
|
It("returns false when folder has multiple types of content", func() {
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{name: "test.mp3"}
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{name: "cover.jpg"}
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{name: "list.m3u"}
|
|
entry.numSubFolders = 3
|
|
Expect(entry.hasNoFiles()).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Describe("isEmpty", func() {
|
|
It("returns true when folder has no files or subfolders", func() {
|
|
Expect(entry.isEmpty()).To(BeTrue())
|
|
})
|
|
It("returns false when folder has audio files", func() {
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{name: "test.mp3"}
|
|
Expect(entry.isEmpty()).To(BeFalse())
|
|
})
|
|
It("returns false when folder has subfolders", func() {
|
|
entry.numSubFolders = 1
|
|
Expect(entry.isEmpty()).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Describe("isNew", func() {
|
|
It("returns true when updTime is zero", func() {
|
|
entry.updTime = time.Time{}
|
|
Expect(entry.isNew()).To(BeTrue())
|
|
})
|
|
|
|
It("returns false when updTime is not zero", func() {
|
|
entry.updTime = time.Now()
|
|
Expect(entry.isNew()).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Describe("toFolder", func() {
|
|
BeforeEach(func() {
|
|
entry.audioFiles = map[string]fs.DirEntry{
|
|
"song1.mp3": &fakeDirEntry{name: "song1.mp3"},
|
|
"song2.mp3": &fakeDirEntry{name: "song2.mp3"},
|
|
}
|
|
entry.imageFiles = map[string]fs.DirEntry{
|
|
"cover.jpg": &fakeDirEntry{name: "cover.jpg"},
|
|
"folder.png": &fakeDirEntry{name: "folder.png"},
|
|
}
|
|
entry.playlistFiles = map[string]fs.DirEntry{
|
|
"list1.m3u": &fakeDirEntry{name: "list1.m3u"},
|
|
"list2.m3u": &fakeDirEntry{name: "list2.m3u"},
|
|
"list3.m3u": &fakeDirEntry{name: "list3.m3u"},
|
|
}
|
|
entry.imagesUpdatedAt = time.Now()
|
|
})
|
|
|
|
It("converts folder entry to model.Folder correctly", func() {
|
|
folder := entry.toFolder()
|
|
|
|
Expect(folder.LibraryID).To(Equal(lib.ID))
|
|
Expect(folder.ID).To(Equal(entry.id))
|
|
Expect(folder.NumAudioFiles).To(Equal(2))
|
|
Expect(folder.ImageFiles).To(ConsistOf("cover.jpg", "folder.png"))
|
|
Expect(folder.ImagesUpdatedAt).To(Equal(entry.imagesUpdatedAt))
|
|
Expect(folder.Hash).To(Equal(entry.hash()))
|
|
})
|
|
|
|
It("sets NumPlaylists when folder is in playlists path", func() {
|
|
// Mock InPlaylistsPath to return true by setting empty PlaylistsPath
|
|
originalPath := conf.Server.PlaylistsPath
|
|
conf.Server.PlaylistsPath = ""
|
|
DeferCleanup(func() { conf.Server.PlaylistsPath = originalPath })
|
|
|
|
folder := entry.toFolder()
|
|
Expect(folder.NumPlaylists).To(Equal(3))
|
|
})
|
|
|
|
It("does not set NumPlaylists when folder is not in playlists path", func() {
|
|
// Mock InPlaylistsPath to return false by setting a different path
|
|
originalPath := conf.Server.PlaylistsPath
|
|
conf.Server.PlaylistsPath = "different/path"
|
|
DeferCleanup(func() { conf.Server.PlaylistsPath = originalPath })
|
|
|
|
folder := entry.toFolder()
|
|
Expect(folder.NumPlaylists).To(BeZero())
|
|
})
|
|
})
|
|
|
|
Describe("hash", func() {
|
|
BeforeEach(func() {
|
|
entry.modTime = time.Date(2023, 1, 15, 12, 0, 0, 0, time.UTC)
|
|
entry.imagesUpdatedAt = time.Date(2023, 1, 16, 14, 30, 0, 0, time.UTC)
|
|
})
|
|
|
|
It("produces deterministic hash for same content", func() {
|
|
entry.audioFiles = map[string]fs.DirEntry{
|
|
"b.mp3": &fakeDirEntry{name: "b.mp3"},
|
|
"a.mp3": &fakeDirEntry{name: "a.mp3"},
|
|
}
|
|
entry.imageFiles = map[string]fs.DirEntry{
|
|
"z.jpg": &fakeDirEntry{name: "z.jpg"},
|
|
"x.png": &fakeDirEntry{name: "x.png"},
|
|
}
|
|
entry.playlistFiles = map[string]fs.DirEntry{
|
|
"q.m3u": &fakeDirEntry{name: "q.m3u"},
|
|
"p.m3u": &fakeDirEntry{name: "p.m3u"},
|
|
}
|
|
entry.numSubFolders = 3
|
|
|
|
hash1 := entry.hash()
|
|
|
|
// Reverse order of maps
|
|
entry.audioFiles = map[string]fs.DirEntry{
|
|
"a.mp3": &fakeDirEntry{name: "a.mp3"},
|
|
"b.mp3": &fakeDirEntry{name: "b.mp3"},
|
|
}
|
|
entry.imageFiles = map[string]fs.DirEntry{
|
|
"x.png": &fakeDirEntry{name: "x.png"},
|
|
"z.jpg": &fakeDirEntry{name: "z.jpg"},
|
|
}
|
|
entry.playlistFiles = map[string]fs.DirEntry{
|
|
"p.m3u": &fakeDirEntry{name: "p.m3u"},
|
|
"q.m3u": &fakeDirEntry{name: "q.m3u"},
|
|
}
|
|
|
|
hash2 := entry.hash()
|
|
Expect(hash1).To(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when audio files change", func() {
|
|
entry.audioFiles = map[string]fs.DirEntry{
|
|
"song1.mp3": &fakeDirEntry{name: "song1.mp3"},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.audioFiles["song2.mp3"] = &fakeDirEntry{name: "song2.mp3"}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when image files change", func() {
|
|
entry.imageFiles = map[string]fs.DirEntry{
|
|
"cover.jpg": &fakeDirEntry{name: "cover.jpg"},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.imageFiles["folder.png"] = &fakeDirEntry{name: "folder.png"}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when modification time changes", func() {
|
|
hash1 := entry.hash()
|
|
|
|
entry.modTime = entry.modTime.Add(1 * time.Hour)
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when playlist files change", func() {
|
|
hash1 := entry.hash()
|
|
|
|
entry.playlistFiles["new.m3u"] = &fakeDirEntry{name: "new.m3u"}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when subfolder count changes", func() {
|
|
hash1 := entry.hash()
|
|
|
|
entry.numSubFolders = 3
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when images updated time changes", func() {
|
|
hash1 := entry.hash()
|
|
|
|
entry.imagesUpdatedAt = entry.imagesUpdatedAt.Add(2 * time.Hour)
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when audio file size changes", func() {
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{
|
|
name: "test.mp3",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "test.mp3",
|
|
size: 1000,
|
|
modTime: time.Now(),
|
|
},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{
|
|
name: "test.mp3",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "test.mp3",
|
|
size: 2000, // Different size
|
|
modTime: time.Now(),
|
|
},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when audio file modification time changes", func() {
|
|
baseTime := time.Now()
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{
|
|
name: "test.mp3",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "test.mp3",
|
|
size: 1000,
|
|
modTime: baseTime,
|
|
},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.audioFiles["test.mp3"] = &fakeDirEntry{
|
|
name: "test.mp3",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "test.mp3",
|
|
size: 1000,
|
|
modTime: baseTime.Add(1 * time.Hour), // Different modtime
|
|
},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when image file size changes", func() {
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{
|
|
name: "cover.jpg",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "cover.jpg",
|
|
size: 5000,
|
|
modTime: time.Now(),
|
|
},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{
|
|
name: "cover.jpg",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "cover.jpg",
|
|
size: 6000, // Different size
|
|
modTime: time.Now(),
|
|
},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when image file modification time changes", func() {
|
|
baseTime := time.Now()
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{
|
|
name: "cover.jpg",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "cover.jpg",
|
|
size: 5000,
|
|
modTime: baseTime,
|
|
},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{
|
|
name: "cover.jpg",
|
|
fileInfo: &fakeFileInfo{
|
|
name: "cover.jpg",
|
|
size: 5000,
|
|
modTime: baseTime.Add(1 * time.Hour), // Different modtime
|
|
},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when playlist file size changes", func() {
|
|
baseTime := time.Now()
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{
|
|
name: "list.m3u",
|
|
fileInfo: &fakeFileInfo{name: "list.m3u", size: 1000, modTime: baseTime},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{
|
|
name: "list.m3u",
|
|
fileInfo: &fakeFileInfo{name: "list.m3u", size: 2000, modTime: baseTime},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when playlist file modification time changes", func() {
|
|
baseTime := time.Now()
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{
|
|
name: "list.m3u",
|
|
fileInfo: &fakeFileInfo{name: "list.m3u", size: 1000, modTime: baseTime},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{
|
|
name: "list.m3u",
|
|
fileInfo: &fakeFileInfo{name: "list.m3u", size: 1000, modTime: baseTime.Add(1 * time.Hour)},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces different hash when a playlist is renamed", func() {
|
|
baseTime := time.Now()
|
|
entry.playlistFiles["old.m3u"] = &fakeDirEntry{
|
|
name: "old.m3u",
|
|
fileInfo: &fakeFileInfo{name: "old.m3u", size: 1000, modTime: baseTime},
|
|
}
|
|
hash1 := entry.hash()
|
|
|
|
delete(entry.playlistFiles, "old.m3u")
|
|
entry.playlistFiles["new.m3u"] = &fakeDirEntry{
|
|
name: "new.m3u",
|
|
fileInfo: &fakeFileInfo{name: "new.m3u", size: 1000, modTime: baseTime},
|
|
}
|
|
hash2 := entry.hash()
|
|
|
|
Expect(hash1).ToNot(Equal(hash2))
|
|
})
|
|
|
|
It("produces valid hex-encoded hash", func() {
|
|
hash := entry.hash()
|
|
Expect(hash).To(HaveLen(32)) // MD5 hash should be 32 hex characters
|
|
Expect(hash).To(MatchRegexp("^[a-f0-9]{32}$"))
|
|
})
|
|
})
|
|
|
|
Describe("isOutdated", func() {
|
|
BeforeEach(func() {
|
|
entry.prevHash = entry.hash()
|
|
})
|
|
|
|
Context("when full scan is in progress", func() {
|
|
BeforeEach(func() {
|
|
entry.job.lib.FullScanInProgress = true
|
|
entry.job.lib.LastScanStartedAt = time.Now()
|
|
})
|
|
|
|
It("returns true when updTime is before LastScanStartedAt", func() {
|
|
entry.updTime = entry.job.lib.LastScanStartedAt.Add(-1 * time.Hour)
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
|
|
It("returns false when updTime is after LastScanStartedAt", func() {
|
|
entry.updTime = entry.job.lib.LastScanStartedAt.Add(1 * time.Hour)
|
|
Expect(entry.isOutdated()).To(BeFalse())
|
|
})
|
|
|
|
It("returns false when updTime equals LastScanStartedAt", func() {
|
|
entry.updTime = entry.job.lib.LastScanStartedAt
|
|
Expect(entry.isOutdated()).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Context("when full scan is not in progress", func() {
|
|
BeforeEach(func() {
|
|
entry.job.lib.FullScanInProgress = false
|
|
})
|
|
|
|
It("returns false when hash hasn't changed", func() {
|
|
Expect(entry.isOutdated()).To(BeFalse())
|
|
})
|
|
|
|
It("returns true when hash has changed", func() {
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{name: "list.m3u"} // Change something to change the hash
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
|
|
It("returns true when prevHash is empty", func() {
|
|
entry.prevHash = ""
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Context("priority between conditions", func() {
|
|
BeforeEach(func() {
|
|
entry.job.lib.FullScanInProgress = true
|
|
entry.job.lib.LastScanStartedAt = time.Now()
|
|
entry.updTime = entry.job.lib.LastScanStartedAt.Add(-1 * time.Hour)
|
|
})
|
|
|
|
It("returns true for full scan condition even when hash hasn't changed", func() {
|
|
// Hash is the same but full scan condition should take priority
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
|
|
It("returns true when full scan condition is not met but hash changed", func() {
|
|
entry.updTime = entry.job.lib.LastScanStartedAt.Add(1 * time.Hour)
|
|
entry.playlistFiles["list.m3u"] = &fakeDirEntry{name: "list.m3u"} // Change hash
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("integration scenarios", func() {
|
|
It("handles complete folder lifecycle", func() {
|
|
// Create new folder entry
|
|
folderPath := "music/rock/album"
|
|
folderID := model.FolderID(lib, folderPath)
|
|
entry := newFolderEntry(job, folderID, folderPath, model.FolderUpdateInfo{})
|
|
|
|
// Initially new and has no files
|
|
Expect(entry.isNew()).To(BeTrue())
|
|
Expect(entry.hasNoFiles()).To(BeTrue())
|
|
|
|
// Add some files
|
|
entry.audioFiles["track1.mp3"] = &fakeDirEntry{name: "track1.mp3"}
|
|
entry.audioFiles["track2.mp3"] = &fakeDirEntry{name: "track2.mp3"}
|
|
entry.imageFiles["cover.jpg"] = &fakeDirEntry{name: "cover.jpg"}
|
|
entry.numSubFolders = 1
|
|
entry.modTime = time.Now()
|
|
entry.imagesUpdatedAt = time.Now()
|
|
|
|
// No longer empty
|
|
Expect(entry.hasNoFiles()).To(BeFalse())
|
|
|
|
// Set previous hash to current hash (simulating it's been saved)
|
|
entry.prevHash = entry.hash()
|
|
entry.updTime = time.Now()
|
|
|
|
// Should not be new or outdated
|
|
Expect(entry.isNew()).To(BeFalse())
|
|
Expect(entry.isOutdated()).To(BeFalse())
|
|
|
|
// Convert to model folder
|
|
folder := entry.toFolder()
|
|
Expect(folder.NumAudioFiles).To(Equal(2))
|
|
Expect(folder.ImageFiles).To(HaveLen(1))
|
|
Expect(folder.Hash).To(Equal(entry.hash()))
|
|
|
|
// Modify folder and verify it becomes outdated
|
|
entry.audioFiles["track3.mp3"] = &fakeDirEntry{name: "track3.mp3"}
|
|
Expect(entry.isOutdated()).To(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
// fakeDirEntry implements fs.DirEntry for testing
|
|
type fakeDirEntry struct {
|
|
name string
|
|
isDir bool
|
|
typ fs.FileMode
|
|
fileInfo fs.FileInfo
|
|
}
|
|
|
|
func (f *fakeDirEntry) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
func (f *fakeDirEntry) IsDir() bool {
|
|
return f.isDir
|
|
}
|
|
|
|
func (f *fakeDirEntry) Type() fs.FileMode {
|
|
return f.typ
|
|
}
|
|
|
|
func (f *fakeDirEntry) Info() (fs.FileInfo, error) {
|
|
if f.fileInfo != nil {
|
|
return f.fileInfo, nil
|
|
}
|
|
return &fakeFileInfo{
|
|
name: f.name,
|
|
isDir: f.isDir,
|
|
mode: f.typ,
|
|
}, nil
|
|
}
|
|
|
|
// fakeFileInfo implements fs.FileInfo for testing
|
|
type fakeFileInfo struct {
|
|
name string
|
|
size int64
|
|
mode fs.FileMode
|
|
modTime time.Time
|
|
isDir bool
|
|
}
|
|
|
|
func (f *fakeFileInfo) Name() string { return f.name }
|
|
func (f *fakeFileInfo) Size() int64 { return f.size }
|
|
func (f *fakeFileInfo) Mode() fs.FileMode { return f.mode }
|
|
func (f *fakeFileInfo) ModTime() time.Time { return f.modTime }
|
|
func (f *fakeFileInfo) IsDir() bool { return f.isDir }
|
|
func (f *fakeFileInfo) Sys() any { return nil }
|