diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-07 00:03:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-07 00:03:30 +0300 |
| commit | 973c3108b71063ea932aed6c67e16e393b5e6c41 (patch) | |
| tree | 0fdfc73724b1478aa6993fc4ef2e6dd6c2e4cc19 /internal | |
| parent | 1f162963c0950cdf08858940cf43e21e5741937d (diff) | |
task 81: replace test sleeps with deterministic sync
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/model/scan_test.go | 17 | ||||
| -rw-r--r-- | internal/service/admin_test.go | 71 | ||||
| -rw-r--r-- | internal/service/scan.go | 12 |
3 files changed, 60 insertions, 40 deletions
diff --git a/internal/model/scan_test.go b/internal/model/scan_test.go index cc0ba6b..acd1dd1 100644 --- a/internal/model/scan_test.go +++ b/internal/model/scan_test.go @@ -2,8 +2,8 @@ package model import ( "errors" + "sync" "testing" - "time" ) func TestScanProgress_Start(t *testing.T) { @@ -116,8 +116,11 @@ func TestScanProgress_ConcurrentAccess(t *testing.T) { p.Start(2) p.SetFilesTotal(100) + start := make(chan struct{}) done := make(chan struct{}) + var copies sync.WaitGroup go func() { + <-start for i := 0; i < 50; i++ { p.IncrementFile() } @@ -125,9 +128,15 @@ func TestScanProgress_ConcurrentAccess(t *testing.T) { }() for i := 0; i < 50; i++ { - _ = p.Copy() - time.Sleep(time.Microsecond) - } + copies.Add(1) + go func() { + defer copies.Done() + <-start + _ = p.Copy() + }() + } + close(start) + copies.Wait() <-done cp := p.Copy() diff --git a/internal/service/admin_test.go b/internal/service/admin_test.go index 17f5373..03a177b 100644 --- a/internal/service/admin_test.go +++ b/internal/service/admin_test.go @@ -22,6 +22,15 @@ func (f *fakeScanner) Scan(ctx context.Context, root string, progress *model.Sca return nil } +func setScanDoneCh(t *testing.T, svc AdminService, doneCh chan<- struct{}) { + t.Helper() + adminSvc, ok := svc.(*adminService) + if !ok { + t.Fatalf("expected *adminService, got %T", svc) + } + adminSvc.scanService.doneCh = doneCh +} + type fakeHasher struct { fixed string err error @@ -300,35 +309,29 @@ func TestAdminService_TriggerRescan_CancelsPrevious(t *testing.T) { func TestAdminService_TriggerRescan_FreshProgressPerScan(t *testing.T) { ctx := context.Background() + started := make(chan struct{}) done := make(chan struct{}) sc := &fakeScanner{ scanFunc: func(_ context.Context, _ string, progress *model.ScanProgress) error { progress.Start(5) + close(started) <-done return nil }, } svc := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, sc, "/media", ctx) + scanDone := make(chan struct{}, 1) + setScanDoneCh(t, svc, scanDone) if err := svc.TriggerRescan(ctx); err != nil { t.Fatalf("unexpected error: %v", err) } - // Wait for goroutine to start. - for { - p := svc.ScanProgress(ctx) - if p.Running { - break - } - time.Sleep(10 * time.Millisecond) - } + <-started close(done) - // Wait for goroutine to finish. - for { - p := svc.ScanProgress(ctx) - if !p.Running { - break - } - time.Sleep(10 * time.Millisecond) + select { + case <-scanDone: + case <-time.After(time.Second): + t.Fatal("timeout waiting for first scan to finish") } p1 := svc.ScanProgress(ctx) @@ -337,10 +340,12 @@ func TestAdminService_TriggerRescan_FreshProgressPerScan(t *testing.T) { } // Start a new scan on the same service with different progress. + started2 := make(chan struct{}) done2 := make(chan struct{}) sc2 := &fakeScanner{ scanFunc: func(_ context.Context, _ string, progress *model.ScanProgress) error { progress.Start(10) + close(started2) <-done2 return nil }, @@ -348,25 +353,17 @@ func TestAdminService_TriggerRescan_FreshProgressPerScan(t *testing.T) { // We replace the scanner field via reflection? No, easier: just create new service. // Actually, the test verifies per-service fresh progress, so new service is fine. svc2 := NewAdminService(&repository.MockStore{}, newMockClock(), &fakeHasher{fixed: "hash"}, sc2, "/media", ctx) + scanDone2 := make(chan struct{}, 1) + setScanDoneCh(t, svc2, scanDone2) if err := svc2.TriggerRescan(ctx); err != nil { t.Fatalf("unexpected error: %v", err) } - // Wait for goroutine to start. - for { - p := svc2.ScanProgress(ctx) - if p.Running { - break - } - time.Sleep(10 * time.Millisecond) - } + <-started2 close(done2) - // Wait for goroutine to finish. - for { - p := svc2.ScanProgress(ctx) - if !p.Running { - break - } - time.Sleep(10 * time.Millisecond) + select { + case <-scanDone2: + case <-time.After(time.Second): + t.Fatal("timeout waiting for second scan to finish") } p2 := svc2.ScanProgress(ctx) @@ -392,12 +389,14 @@ func TestAdminService_TriggerRescan_ConcurrentCalls(t *testing.T) { var wg sync.WaitGroup callCount := 0 var mu sync.Mutex + started := make(chan struct{}, 5) sc := &fakeScanner{ scanFunc: func(scanCtx context.Context, _ string, progress *model.ScanProgress) error { mu.Lock() callCount++ mu.Unlock() progress.Start(1) + started <- struct{}{} <-scanCtx.Done() return scanCtx.Err() }, @@ -413,13 +412,13 @@ func TestAdminService_TriggerRescan_ConcurrentCalls(t *testing.T) { } wg.Wait() - // Wait for the final surviving goroutine to start. - for { - progress := svc.ScanProgress(ctx) - if progress.Running { - break + timeout := time.After(time.Second) + for !svc.ScanProgress(ctx).Running { + select { + case <-started: + case <-timeout: + t.Fatal("timeout waiting for a running scan") } - time.Sleep(10 * time.Millisecond) } mu.Lock() diff --git a/internal/service/scan.go b/internal/service/scan.go index d019616..52bc5c5 100644 --- a/internal/service/scan.go +++ b/internal/service/scan.go @@ -22,6 +22,7 @@ type scanService struct { scanCancel context.CancelFunc progress *model.ScanProgress appCtx context.Context // application-level context used to propagate shutdown cancellation + doneCh chan<- struct{} } // NewScanService creates a ScanService. @@ -64,6 +65,7 @@ func (s *scanService) TriggerRescan(ctx context.Context) error { progress.Done(nil) s.logger.Info("rescan completed") } + s.notifyDone() }() return nil } @@ -77,3 +79,13 @@ func (s *scanService) ScanProgress(ctx context.Context) model.ScanProgress { } return progress.Copy() } + +func (s *scanService) notifyDone() { + if s.doneCh == nil { + return + } + select { + case s.doneCh <- struct{}{}: + default: + } +} |
