From 8cd23e6cfcbd0b0c93463ede5e1b7f134a0c285f Mon Sep 17 00:00:00 2001 From: georgehao Date: Wed, 26 Feb 2025 00:47:46 +0800 Subject: [PATCH] add unit test to ClosableMutex --- internal/syncx/mutex_test.go | 124 +++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 internal/syncx/mutex_test.go diff --git a/internal/syncx/mutex_test.go b/internal/syncx/mutex_test.go new file mode 100644 index 0000000000..def97239a6 --- /dev/null +++ b/internal/syncx/mutex_test.go @@ -0,0 +1,124 @@ +package syncx + +import ( + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestClosableMutex(t *testing.T) { + t.Run("basic lock/unlock", func(t *testing.T) { + cm := NewClosableMutex() + if !cm.TryLock() { + t.Error("TryLock should succeed on new mutex") + } + cm.Unlock() + }) + + t.Run("multiple lock/unlock", func(t *testing.T) { + cm := NewClosableMutex() + for i := 0; i < 3; i++ { + if !cm.TryLock() { + t.Error("TryLock should succeed") + } + cm.Unlock() + } + }) + + t.Run("concurrent lock/unlock", func(t *testing.T) { + cm := NewClosableMutex() + var wg sync.WaitGroup + + // lockCount is used to count how many goroutines have acquired the lock + var lockCount atomic.Int32 + + // Start a goroutine that holds the lock for a short time + wg.Add(2) + go func() { + defer wg.Done() + if !cm.TryLock() { + t.Error("First TryLock should succeed") + return + } + time.Sleep(3 * time.Second) + lockCount.Add(1) + cm.Unlock() + }() + + go func() { + defer wg.Done() + // if main goroutine acquires the lock, it will increment lockCount + // so check the lockCount at the second to see if the main goroutine has acquired the lock + time.Sleep(2 * time.Second) + if lockCount.Load() != 0 { + t.Error("Second TryLock should not succeed while the first goroutine holds the lock") + } + }() + + // Try to acquire the lock while it's held + time.Sleep(time.Second) // Wait for the first goroutine to acquire the lock + + // NOTE: will block here until the first goroutine releases the lock + if !cm.TryLock() { + t.Error("Second TryLock should block here unitl the first goroutine releases the lock, but it didn't") + cm.Unlock() + } + + lockCount.Add(1) + + // the main goroutine will increment lockCount once the first gourotine releases the lock, + // so lockCount should be 2 + if lockCount.Load() != 2 { + t.Error("main gourotine should have acquired the lock") + } + + wg.Wait() + }) + + t.Run("must lock", func(t *testing.T) { + cm := NewClosableMutex() + cm.MustLock() + cm.Unlock() + }) + + t.Run("close mutex", func(t *testing.T) { + cm := NewClosableMutex() + cm.Close() + if cm.TryLock() { + t.Error("TryLock should fail on closed mutex") + } + }) + + t.Run("panic on unlock of unlocked mutex", func(t *testing.T) { + cm := NewClosableMutex() + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic on Unlock of unlocked mutex") + } + }() + cm.Unlock() // Should panic + }) + + t.Run("panic on must lock of closed mutex", func(t *testing.T) { + cm := NewClosableMutex() + cm.Close() + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic on MustLock of closed mutex") + } + }() + cm.MustLock() // Should panic + }) + + t.Run("panic on close of closed mutex", func(t *testing.T) { + cm := NewClosableMutex() + cm.Close() + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic on Close of closed mutex") + } + }() + cm.Close() // Should panic + }) +}