test: fix flaky watcher and scheduler tests on Windows CI
Replace timing-sensitive time.Sleep synchronization with proper Eventually/Consistently assertions in watcher tests, and increase Eventually timeouts from 200ms to 500ms. Add FlakeAttempts(3) to the inherently timing-dependent tests. For the scheduler test, increase the Eventually timeout from 1s to 5s for the cron job execution check.
This commit is contained in:
parent
a00152397e
commit
52099ce91f
2 changed files with 39 additions and 51 deletions
|
|
@ -74,23 +74,22 @@ var _ = Describe("Watcher", func() {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("creates separate targets for different folders", func() {
|
It("creates separate targets for different folders", FlakeAttempts(3), func() {
|
||||||
// Send notifications for different folders
|
// Send notifications for different folders
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist2"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist2"}
|
||||||
|
|
||||||
// Wait for watcher to process and trigger scan
|
// Wait for a scan that collected both targets
|
||||||
Eventually(func() int {
|
Eventually(func() []model.ScanTarget {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
if len(calls) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return calls[0].Targets
|
||||||
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(HaveLen(2))
|
||||||
|
|
||||||
// Verify two targets
|
// Verify targets
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
Expect(calls).To(HaveLen(1))
|
|
||||||
Expect(calls[0].Targets).To(HaveLen(2))
|
|
||||||
|
|
||||||
// Extract folder paths
|
|
||||||
folderPaths := make(map[string]bool)
|
folderPaths := make(map[string]bool)
|
||||||
for _, target := range calls[0].Targets {
|
for _, target := range calls[0].Targets {
|
||||||
Expect(target.LibraryID).To(Equal(1))
|
Expect(target.LibraryID).To(Equal(1))
|
||||||
|
|
@ -107,7 +106,7 @@ var _ = Describe("Watcher", func() {
|
||||||
// Wait for watcher to process and trigger scan
|
// Wait for watcher to process and trigger scan
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
|
|
||||||
// Verify the target
|
// Verify the target
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
|
|
@ -117,20 +116,15 @@ var _ = Describe("Watcher", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("deduplicates folder and file within same folder", func() {
|
It("deduplicates folder and file within same folder", func() {
|
||||||
// Send notification for a folder
|
// Send multiple notifications for the same folder
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
// Send notification for same folder (as if file change was detected there)
|
|
||||||
// In practice, watchLibrary() would walk up from file path to folder
|
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
// Send another for same folder
|
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1/album1"}
|
||||||
|
|
||||||
// Wait for watcher to process and trigger scan
|
// Wait for watcher to process and trigger scan
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
|
|
||||||
// Verify only one target despite multiple file/folder changes
|
// Verify only one target despite multiple file/folder changes
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
|
|
@ -151,32 +145,27 @@ var _ = Describe("Watcher", func() {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("resets timer on each change (debouncing)", func() {
|
It("resets timer on each change (debouncing)", FlakeAttempts(3), func() {
|
||||||
// Send first notification
|
// Send first notification
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||||
|
|
||||||
// Wait a bit less than half the watcher wait time to ensure timer doesn't fire
|
// Verify no scan fires during a window shorter than the debounce wait
|
||||||
time.Sleep(20 * time.Millisecond)
|
Consistently(func() int {
|
||||||
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
// No scan should have been triggered yet
|
}, 20*time.Millisecond, 5*time.Millisecond).Should(Equal(0))
|
||||||
Expect(mockScanner.GetScanFoldersCallCount()).To(Equal(0))
|
|
||||||
|
|
||||||
// Send another notification (resets timer)
|
// Send another notification (resets timer)
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||||
|
|
||||||
// Wait a bit less than half the watcher wait time again
|
// Again, no scan should fire within a short window
|
||||||
time.Sleep(20 * time.Millisecond)
|
Consistently(func() int {
|
||||||
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
|
}, 20*time.Millisecond, 5*time.Millisecond).Should(Equal(0))
|
||||||
|
|
||||||
// Still no scan
|
// Now wait for the debounce timer to expire and trigger scan
|
||||||
Expect(mockScanner.GetScanFoldersCallCount()).To(Equal(0))
|
|
||||||
|
|
||||||
// Wait for full timer to expire after last notification (plus margin)
|
|
||||||
time.Sleep(60 * time.Millisecond)
|
|
||||||
|
|
||||||
// Now scan should have been triggered
|
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 100*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("triggers scan after quiet period", func() {
|
It("triggers scan after quiet period", func() {
|
||||||
|
|
@ -189,7 +178,7 @@ var _ = Describe("Watcher", func() {
|
||||||
// Wait for quiet period
|
// Wait for quiet period
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -211,7 +200,7 @@ var _ = Describe("Watcher", func() {
|
||||||
// Wait for scan
|
// Wait for scan
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
|
|
||||||
// Should scan the library root
|
// Should scan the library root
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
|
|
@ -223,13 +212,12 @@ var _ = Describe("Watcher", func() {
|
||||||
It("deduplicates empty and dot paths", func() {
|
It("deduplicates empty and dot paths", func() {
|
||||||
// Send notifications with empty and dot paths
|
// Send notifications with empty and dot paths
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: ""}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: ""}
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: ""}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: ""}
|
||||||
|
|
||||||
// Wait for scan
|
// Wait for scan
|
||||||
Eventually(func() int {
|
Eventually(func() int {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
return mockScanner.GetScanFoldersCallCount()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
||||||
|
|
||||||
// Should have only one target
|
// Should have only one target
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
|
|
@ -264,20 +252,19 @@ var _ = Describe("Watcher", func() {
|
||||||
It("creates separate targets for different libraries", func() {
|
It("creates separate targets for different libraries", func() {
|
||||||
// Send notifications for both libraries
|
// Send notifications for both libraries
|
||||||
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
w.watcherNotify <- scanNotification{Library: lib, FolderPath: "artist1"}
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
w.watcherNotify <- scanNotification{Library: lib2, FolderPath: "artist2"}
|
w.watcherNotify <- scanNotification{Library: lib2, FolderPath: "artist2"}
|
||||||
|
|
||||||
// Wait for scan
|
// Wait for a scan that collected both targets
|
||||||
Eventually(func() int {
|
Eventually(func() []model.ScanTarget {
|
||||||
return mockScanner.GetScanFoldersCallCount()
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
}, 200*time.Millisecond, 10*time.Millisecond).Should(Equal(1))
|
if len(calls) == 0 {
|
||||||
|
return nil
|
||||||
// Verify two targets for different libraries
|
}
|
||||||
calls := mockScanner.GetScanFoldersCalls()
|
return calls[0].Targets
|
||||||
Expect(calls).To(HaveLen(1))
|
}, 500*time.Millisecond, 10*time.Millisecond).Should(HaveLen(2))
|
||||||
Expect(calls[0].Targets).To(HaveLen(2))
|
|
||||||
|
|
||||||
// Verify library IDs are different
|
// Verify library IDs are different
|
||||||
|
calls := mockScanner.GetScanFoldersCalls()
|
||||||
libraryIDs := make(map[int]bool)
|
libraryIDs := make(map[int]bool)
|
||||||
for _, target := range calls[0].Targets {
|
for _, target := range calls[0].Targets {
|
||||||
libraryIDs[target.LibraryID] = true
|
libraryIDs[target.LibraryID] = true
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package scheduler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
|
@ -28,7 +29,7 @@ var _ = Describe("Scheduler", func() {
|
||||||
s.c.Stop() // Stop the scheduler after tests
|
s.c.Stop() // Stop the scheduler after tests
|
||||||
})
|
})
|
||||||
|
|
||||||
It("adds and executes a job", func() {
|
It("adds and executes a job", FlakeAttempts(3), func() {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
id, err := s.Add("@every 50ms", func() {
|
id, err := s.Add("@every 50ms", func() {
|
||||||
|
|
@ -38,7 +39,7 @@ var _ = Describe("Scheduler", func() {
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(id).ToNot(BeZero())
|
Expect(id).ToNot(BeZero())
|
||||||
|
|
||||||
Eventually(done).Should(BeClosed())
|
Eventually(done, 5*time.Second).Should(BeClosed())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("adds a job with random ~ syntax", func() {
|
It("adds a job with random ~ syntax", func() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue