From 606473d0fda1fd4589a4272216a6bd3cc4b9d555 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 16:28:38 +0200 Subject: [PATCH] test(analyze): race-check StoredDir stats against concurrent readers --- pkg/analyze/stored_race_test.go | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pkg/analyze/stored_race_test.go diff --git a/pkg/analyze/stored_race_test.go b/pkg/analyze/stored_race_test.go new file mode 100644 index 0000000..c65b0f7 --- /dev/null +++ b/pkg/analyze/stored_race_test.go @@ -0,0 +1,58 @@ +package analyze + +import ( + "sync" + "testing" + + "github.com/dundee/gdu/v5/internal/testdir" + "github.com/dundee/gdu/v5/pkg/fs" + "github.com/stretchr/testify/assert" +) + +// TestStoredDirUpdateStatsLockedAgainstReaders recomputes a StoredDir's stats +// while another goroutine reads them through the synchronized scalar accessors +// (GetSize, GetUsage, GetFlag, GetMtime, GetItemCount). Those readers take the +// embedded Dir mutex, so updateStats has to write the same fields under that +// mutex. Run with -race: without the locking the writes in updateStats race the +// reads here. +func TestStoredDirUpdateStatsLockedAgainstReaders(t *testing.T) { + fin := testdir.CreateTestDir() + defer fin() + + a := CreateStoredAnalyzer("/tmp/badger") + dir := a.AnalyzeDir( + "test_dir", func(_, _ string) bool { return false }, func(_ string) bool { return false }, + ).(*StoredDir) + a.GetDone().Wait() + + const rounds = 200 + + stop := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stop: + return + default: + } + _ = dir.GetSize() + _ = dir.GetUsage() + _ = dir.GetFlag() + _ = dir.GetMtime() + _ = dir.GetItemCount() + } + }() + + for range rounds { + dir.UpdateStats(make(fs.HardLinkedItems)) + } + close(stop) + wg.Wait() + + // stats stay correct after the concurrent recomputation + assert.Equal(t, int64(7+4096*3), dir.GetSize()) + assert.Equal(t, int64(5), dir.GetItemCount()) +}