From ed03e3ec64f8d1eb534cc96aff34773c26e3a967 Mon Sep 17 00:00:00 2001 From: suleman Date: Fri, 25 Sep 2026 15:22:07 +0500 Subject: [PATCH] added unit tests --- .../DistributedLock.NCache.slnx | 1 + .../NCache.DistributedLock.csproj | 3 + .../Tests/IDistribtuedLockTest.csproj | 41 +++ .../Tests/KeyGenerationTesting.cs | 130 +++++++++ .../Tests/MutexLockTesting.cs | 236 ++++++++++++++++ NCache.OSS.DistributedLock/Tests/Program.cs | 79 ++++++ .../Tests/ReaderWriterLockTesting.cs | 267 ++++++++++++++++++ .../Tests/SemaphoreLockTesting.cs | 241 ++++++++++++++++ .../Tests/TestRunner.cs | 135 +++++++++ .../Tests/client.ncconf | 7 + .../Tests/config.ncconf | 14 + 11 files changed, 1154 insertions(+) create mode 100644 NCache.OSS.DistributedLock/Tests/IDistribtuedLockTest.csproj create mode 100644 NCache.OSS.DistributedLock/Tests/KeyGenerationTesting.cs create mode 100644 NCache.OSS.DistributedLock/Tests/MutexLockTesting.cs create mode 100644 NCache.OSS.DistributedLock/Tests/Program.cs create mode 100644 NCache.OSS.DistributedLock/Tests/ReaderWriterLockTesting.cs create mode 100644 NCache.OSS.DistributedLock/Tests/SemaphoreLockTesting.cs create mode 100644 NCache.OSS.DistributedLock/Tests/TestRunner.cs create mode 100644 NCache.OSS.DistributedLock/Tests/client.ncconf create mode 100644 NCache.OSS.DistributedLock/Tests/config.ncconf diff --git a/NCache.OSS.DistributedLock/DistributedLock.NCache.slnx b/NCache.OSS.DistributedLock/DistributedLock.NCache.slnx index 2f4ea3f..ef31560 100644 --- a/NCache.OSS.DistributedLock/DistributedLock.NCache.slnx +++ b/NCache.OSS.DistributedLock/DistributedLock.NCache.slnx @@ -1,4 +1,5 @@ + \ No newline at end of file diff --git a/NCache.OSS.DistributedLock/NCache.DistributedLock.csproj b/NCache.OSS.DistributedLock/NCache.DistributedLock.csproj index 74cf572..84dcd91 100644 --- a/NCache.OSS.DistributedLock/NCache.DistributedLock.csproj +++ b/NCache.OSS.DistributedLock/NCache.DistributedLock.csproj @@ -8,6 +8,9 @@ 5.3.6.1 + + + $(DefaultItemExcludes);Tests/** diff --git a/NCache.OSS.DistributedLock/Tests/IDistribtuedLockTest.csproj b/NCache.OSS.DistributedLock/Tests/IDistribtuedLockTest.csproj new file mode 100644 index 0000000..d620356 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/IDistribtuedLockTest.csproj @@ -0,0 +1,41 @@ + + + + Exe + net8.0;net48 + enable + enable + latest + true + + + + true + + + + + true + + + + + + + + + + + + + + Always + + + Always + + + + \ No newline at end of file diff --git a/NCache.OSS.DistributedLock/Tests/KeyGenerationTesting.cs b/NCache.OSS.DistributedLock/Tests/KeyGenerationTesting.cs new file mode 100644 index 0000000..d8521b0 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/KeyGenerationTesting.cs @@ -0,0 +1,130 @@ +using NCache.DistributedLock.Common; + +namespace IDistribtuedLockTest +{ + /// + /// Unit tests for the internal KeyGeneration utility. + /// These tests are pure in-process and do NOT need a live NCache instance. + /// Reflection is used to exercise the internal class. + /// + internal class KeyGenerationTesting + { + public static void RunTests() + { + Console.WriteLine(); + Console.WriteLine("══ KeyGeneration Unit Tests (no NCache needed) ══════════════"); + + // Retrieve the private type via reflection so we can call internal methods + var assembly = typeof(NCache.DistributedLock.Locks.NCacheDistributedLock).Assembly; + var type = assembly.GetType("DistributedLock.NCache.Common.KeyGeneration")!; + var method = type.GetMethod("GetKey", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; + + // Enum values + var enumType = type.GetNestedType("LockType", + System.Reflection.BindingFlags.NonPublic)!; + var distLock = Enum.Parse(enumType, "DistributedLock"); + var semaphore = Enum.Parse(enumType, "SempahoreLock"); + var readerLock = Enum.Parse(enumType, "ReaderLock"); + var writerLock = Enum.Parse(enumType, "WriterLock"); + + string Invoke(object lockType, string name) + => (string)method.Invoke(null, new[] { lockType, name })!; + + // ── TC-KG-01 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-01: DistributedLock key has correct prefix", + () => + { + var key = Invoke(distLock, "myLock"); + TestRunner.AssertTrue(key.StartsWith("ncache#distLock#"), + $"Expected prefix 'ncache#distLock#' but got '{key}'"); + TestRunner.AssertTrue(key.EndsWith("myLock"), + "Key should end with the lock name"); + }); + + // ── TC-KG-02 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-02: SemaphoreLock key has correct prefix", + () => + { + var key = Invoke(semaphore, "mySemaphore"); + TestRunner.AssertTrue(key.StartsWith("ncache#distSemaphore#"), + $"Expected prefix 'ncache#distSemaphore#' but got '{key}'"); + }); + + // ── TC-KG-03 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-03: ReaderLock key has correct prefix", + () => + { + var key = Invoke(readerLock, "myReader"); + TestRunner.AssertTrue(key.StartsWith("ncache#distReaderLock#"), + $"Expected prefix 'ncache#distReaderLock#' but got '{key}'"); + }); + + // ── TC-KG-04 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-04: WriterLock key has correct prefix", + () => + { + var key = Invoke(writerLock, "myWriter"); + TestRunner.AssertTrue(key.StartsWith("ncache#distWriterLock#"), + $"Expected prefix 'ncache#distWriterLock#' but got '{key}'"); + }); + + // ── TC-KG-05 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-05: Null lock name throws ArgumentException", + () => + { + TestRunner.AssertThrows( + () => Invoke(distLock, null!), + "Null lock name should throw"); + }); + + // ── TC-KG-06 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-06: Empty lock name throws ArgumentException", + () => + { + TestRunner.AssertThrows( + () => Invoke(distLock, ""), + "Empty lock name should throw"); + }); + + // ── TC-KG-07 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-07: Whitespace-only lock name throws ArgumentException", + () => + { + TestRunner.AssertThrows( + () => Invoke(distLock, " "), + "Whitespace-only lock name should throw"); + }); + + // ── TC-KG-08 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-08: Different lock names produce different keys (same type)", + () => + { + var key1 = Invoke(distLock, "lockA"); + var key2 = Invoke(distLock, "lockB"); + TestRunner.AssertFalse(key1 == key2, + "Different names must generate different keys"); + }); + + // ── TC-KG-09 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-09: Same name with different lock types produces different keys", + () => + { + var k1 = Invoke(distLock, "shared"); + var k2 = Invoke(semaphore, "shared"); + TestRunner.AssertFalse(k1 == k2, + "Same name with different lock types must produce different keys"); + }); + + // ── TC-KG-10 ───────────────────────────────────────────────────── + TestRunner.Run("TC-KG-10: Key generation is deterministic for same inputs", + () => + { + var key1 = Invoke(distLock, "stable"); + var key2 = Invoke(distLock, "stable"); + TestRunner.AssertEqual(key1, key2, + "Same lock type + name should always produce the same key"); + }); + } + } +} diff --git a/NCache.OSS.DistributedLock/Tests/MutexLockTesting.cs b/NCache.OSS.DistributedLock/Tests/MutexLockTesting.cs new file mode 100644 index 0000000..38c2ecb --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/MutexLockTesting.cs @@ -0,0 +1,236 @@ +using Alachisoft.NCache.Client; +using NCache.DistributedLock.Providers; +using Medallion.Threading; + +namespace IDistribtuedLockTest +{ + /// + /// Functional / integration smoke-test for the basic distributed mutex lock + /// (NCacheDistributedLock). These tests require a live NCache instance. + /// + internal class MutexLockTesting + { + // ── Entry point kept for backward compatibility ─────────────────────── + + public static void Runner() + { + ICache cache = CacheManager.GetCache("democache"); + + NCacheDistributedSynchronizationProvider provider = new(cache); + + IDistributedLock distLock = provider.CreateLock("criticalsection1"); + + var task1 = Task.Run(() => + { + try + { + using (var handle = distLock.Acquire(TimeSpan.FromSeconds(7))) + { + Console.WriteLine("Lock 1 Acquired"); + Thread.Sleep(5000); + } + Console.WriteLine("Lock 1 Released"); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Console.WriteLine("First Failed"); + } + }); + + var task2 = Task.Run(() => + { + try + { + using (var handle = distLock.Acquire(TimeSpan.FromSeconds(7))) + { + Console.WriteLine("Lock 2 Acquired"); + Thread.Sleep(5000); + } + Console.WriteLine("Lock 2 Released"); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + Console.WriteLine("Second Failed"); + } + }); + + Task.WhenAll(task1, task2).Wait(); + + Console.ReadKey(); + } + + // ── Test suite ──────────────────────────────────────────────────────── + + public static void RunTests(ICache cache) + { + Console.WriteLine(); + Console.WriteLine("══ Mutex (Distributed Lock) Tests ══════════════════════════"); + + var provider = new NCacheDistributedSynchronizationProvider(cache); + + TestRunner.Run("TC-ML-01: Lock acquired returns non-null handle", + () => + { + var lck = provider.CreateLock($"mutex-tc01-{Guid.NewGuid()}"); + using var handle = lck.Acquire(TimeSpan.FromSeconds(5)); + TestRunner.AssertNotNull(handle, "Acquire should return a valid handle"); + }); + + TestRunner.Run("TC-ML-02: Lock is exclusive — second acquire blocks until first is released", + () => + { + string lockName = $"mutex-tc02-{Guid.NewGuid()}"; + var lck = provider.CreateLock(lockName); + + bool task2Ran = false; + bool task1StillHeld = false; + + // Task 1 holds the lock for 3 s + var t1 = Task.Run(() => + { + using var handle = lck.Acquire(TimeSpan.FromSeconds(10)); + Thread.Sleep(3000); + // At this point task 2 should NOT have run yet + task1StillHeld = !task2Ran; + }); + + Thread.Sleep(200); // ensure T1 gets the lock first + + // Task 2 waits up to 7 s + var t2 = Task.Run(() => + { + using var handle = lck.Acquire(TimeSpan.FromSeconds(7)); + task2Ran = true; + }); + + Task.WhenAll(t1, t2).Wait(); + + TestRunner.AssertTrue(task2Ran, "Task 2 should eventually acquire the lock"); + TestRunner.AssertTrue(task1StillHeld, + "Task 2 should not run while Task 1 holds the lock"); + }); + + TestRunner.Run("TC-ML-03: Lock is released when handle is disposed", + () => + { + string lockName = $"mutex-tc03-{Guid.NewGuid()}"; + var lck = provider.CreateLock(lockName); + + // Acquire and immediately dispose + var handle = lck.Acquire(TimeSpan.FromSeconds(5)); + handle.Dispose(); + + // A second acquire should succeed without waiting + bool secondAcquired = false; + var t = Task.Run(() => + { + using var h2 = lck.Acquire(TimeSpan.FromSeconds(3)); + + secondAcquired = true; + }); + + bool completedInTime = t.Wait(TimeSpan.FromSeconds(5)); + TestRunner.AssertTrue(completedInTime, "Second acquire should complete quickly"); + TestRunner.AssertTrue(secondAcquired, "Second handle should be non-null"); + }); + + TestRunner.Run("TC-ML-04: Acquire throws TimeoutException when lock unavailable", + () => + { + string lockName = $"mutex-tc04-{Guid.NewGuid()}"; + var lck = provider.CreateLock(lockName); + + // Hold the lock for 10 s on a background thread + var holderReady = new ManualResetEventSlim(false); + var releaseHolder = new ManualResetEventSlim(false); + + var holder = Task.Run(() => + { + using var h = lck.Acquire(TimeSpan.FromSeconds(15)); + holderReady.Set(); + releaseHolder.Wait(); + }); + + holderReady.Wait(); + + try + { + // Try with a very short timeout — should time out + TestRunner.AssertThrows( + () => lck.Acquire(TimeSpan.FromMilliseconds(500)), + "Should throw TimeoutException when lock cannot be acquired in time"); + } + finally + { + releaseHolder.Set(); + holder.Wait(); + } + }); + + TestRunner.Run("TC-ML-05: Multiple independent locks do not interfere", + () => + { + var lockA = provider.CreateLock($"mutex-tc05-A-{Guid.NewGuid()}"); + var lockB = provider.CreateLock($"mutex-tc05-B-{Guid.NewGuid()}"); + + bool bothAcquired = false; + + using var hA = lockA.Acquire(TimeSpan.FromSeconds(5)); + using var hB = lockB.Acquire(TimeSpan.FromSeconds(5)); + bothAcquired = true; + + TestRunner.AssertTrue(bothAcquired, "Two different locks should be acquirable simultaneously"); + }); + + TestRunner.Run("TC-ML-06: Re-acquire after release succeeds", + () => + { + string lockName = $"mutex-tc06-{Guid.NewGuid()}"; + var lck = provider.CreateLock(lockName); + + for (int i = 0; i < 3; i++) + { + using var handle = lck.Acquire(TimeSpan.FromSeconds(5)); + TestRunner.AssertNotNull(handle, $"Acquire attempt #{i + 1} should succeed"); + } + }); + + TestRunner.Run("TC-ML-07: Two tasks race — exactly one acquires at a time", + () => + { + string lockName = $"mutex-tc07-{Guid.NewGuid()}"; + var lck = provider.CreateLock(lockName); + + int concurrentHolders = 0; + bool exclusivityViolated = false; + var exceptions = new List(); + var tasks = new List(); + + for (int i = 0; i < 4; i++) + { + tasks.Add(Task.Run(() => + { + try + { + using var handle = lck.Acquire(TimeSpan.FromSeconds(15)); + int current = Interlocked.Increment(ref concurrentHolders); + if (current > 1) exclusivityViolated = true; + Thread.Sleep(300); + Interlocked.Decrement(ref concurrentHolders); + } + catch (Exception ex) { lock (exceptions) exceptions.Add(ex); } + })); + } + + Task.WhenAll(tasks).Wait(); + + TestRunner.AssertFalse(exclusivityViolated, + "At no point should more than one thread hold the mutex"); + TestRunner.AssertEqual(0, exceptions.Count, + "No unexpected exceptions should occur"); + }); + } + } +} diff --git a/NCache.OSS.DistributedLock/Tests/Program.cs b/NCache.OSS.DistributedLock/Tests/Program.cs new file mode 100644 index 0000000..25d36b9 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/Program.cs @@ -0,0 +1,79 @@ +using Alachisoft.NCache.Client; +using NCache.DistributedLock.Locks; +using NCache.DistributedLock.Primitives; +using NCache.DistributedLock.Providers; +using IDistribtuedLockTest; +using Medallion.Threading; +using Microsoft.Win32; + +// ════════════════════════════════════════════════════════════════════════════ +// Test Application Entry Point +// +// Usage: +// dotnet run — run all tests (unit + integration) +// dotnet run -- unit — run only unit tests (no NCache connection) +// dotnet run -- legacy — run the original MutexLockTesting.Runner() +// ════════════════════════════════════════════════════════════════════════════ + +string mode = args.Length > 0 ? args[0].ToLowerInvariant() : "all"; + +if (mode == "legacy") +{ + MutexLockTesting.Runner(); + return; +} + +Console.WriteLine("╔══════════════════════════════════════════════════════════╗"); +Console.WriteLine("║ DistributedLock.NCache — Test Suite ║"); +Console.WriteLine("╚══════════════════════════════════════════════════════════╝"); + +// ── Unit tests (always run, no NCache required) ─────────────────────────── +//KeyGenerationTesting.RunTests(); + +if (mode == "unit") +{ + TestRunner.PrintSummary(); + return; +} + +// ── Integration tests (require live NCache) ─────────────────────────────── +Console.WriteLine(); +Console.WriteLine("Connecting to NCache (\"democache\") …"); + +ICache? cache = null; +try +{ + cache = CacheManager.GetCache("democache"); + Console.WriteLine("Connected."); +} +catch (Exception ex) +{ + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"WARNING: Could not connect to NCache — {ex.Message}"); + Console.WriteLine("Skipping all integration tests."); + Console.ResetColor(); + TestRunner.PrintSummary(); + return; +} + +var storing = new DistributedLockAcquisition(TimeSpan.FromSeconds(10)); +cache.Insert("test", storing); +var test = cache.Get("test"); + +if(test != null && test.IsEqual(storing)) +{ + Console.WriteLine("Test Completes"); +} +else +{ + Console.WriteLine("Test Base Fails"); +} + +MutexLockTesting.RunTests(cache); +SemaphoreLockTesting.RunTests(cache); +ReaderWriterLockTesting.RunTests(cache); + +TestRunner.PrintSummary(); + +if (TestRunner.HasFailures) + Environment.Exit(1); diff --git a/NCache.OSS.DistributedLock/Tests/ReaderWriterLockTesting.cs b/NCache.OSS.DistributedLock/Tests/ReaderWriterLockTesting.cs new file mode 100644 index 0000000..6f19588 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/ReaderWriterLockTesting.cs @@ -0,0 +1,267 @@ +using Alachisoft.NCache.Client; +using NCache.DistributedLock.Providers; +using Medallion.Threading; + +namespace IDistribtuedLockTest +{ + /// + /// Integration tests for the distributed reader-writer lock + /// (NCacheDistributedReaderWriterLock). + /// + /// Reader-writer lock contract: + /// • Multiple readers may hold the lock concurrently. + /// • A writer gets exclusive access — no readers and no other writers. + /// • A waiting writer blocks new readers from acquiring. + /// + internal class ReaderWriterLockTesting + { + public static void RunTests(ICache cache) + { + Console.WriteLine(); + Console.WriteLine("══ Reader-Writer Lock Tests ═════════════════════════════════"); + + var provider = new NCacheDistributedSynchronizationProvider(cache); + + // ── TC-RW-01 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-01: Read lock acquire returns a valid handle", + () => + { + var rwl = provider.CreateReaderWriterLock($"rwl-tc01-{Guid.NewGuid()}"); + using (var handle = rwl.AcquireReadLock(TimeSpan.FromSeconds(5))) + { + TestRunner.AssertNotNull(handle, "AcquireReadLock should return a non-null handle"); + } + }); + + // ── TC-RW-02 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-02: Write lock acquire returns a valid handle", + () => + { + var rwl = provider.CreateReaderWriterLock($"rwl-tc02-{Guid.NewGuid()}"); + using var handle = rwl.AcquireWriteLock(TimeSpan.FromSeconds(5)); + TestRunner.AssertNotNull(handle, "AcquireWriteLock should return a non-null handle"); + }); + + // ── TC-RW-03 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-03: Multiple concurrent readers can hold the lock simultaneously", + () => + { + string name = $"rwl-tc03-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + int concurrentReaders = 0; + int maxObserved = 0; + var allReady = new CountdownEvent(3); + var release = new ManualResetEventSlim(false); + + var readers = Enumerable.Range(0, 3).Select(_ => Task.Run(() => + { + using var h = rwl.AcquireReadLock(TimeSpan.FromSeconds(10)); + int val = Interlocked.Increment(ref concurrentReaders); + // Track peak concurrent readers + int observed; + do { observed = maxObserved; } + while (observed < val && Interlocked.CompareExchange(ref maxObserved, val, observed) != observed); + + allReady.Signal(); + release.Wait(); + Interlocked.Decrement(ref concurrentReaders); + })).ToList(); + + allReady.Wait(TimeSpan.FromSeconds(10)); + release.Set(); + Task.WhenAll(readers).Wait(); + + TestRunner.AssertEqual(3, maxObserved, + "All 3 readers should hold the lock at the same time"); + }); + + // ── TC-RW-04 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-04: Writer is exclusive — blocks while readers hold the lock", + () => + { + string name = $"rwl-tc04-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + bool writerRan = false; + bool readerStillHeld = false; + + var readerReady = new ManualResetEventSlim(false); + var releaseReader = new ManualResetEventSlim(false); + + var readerTask = Task.Run(() => + { + using var h = rwl.AcquireReadLock(TimeSpan.FromSeconds(10)); + readerReady.Set(); + releaseReader.Wait(); + }); + + readerReady.Wait(); + + var writerTask = Task.Run(() => + { + using var h = rwl.AcquireWriteLock(TimeSpan.FromSeconds(10)); + writerRan = true; + }); + + Thread.Sleep(500); + readerStillHeld = !writerRan; + releaseReader.Set(); + + Task.WhenAll(readerTask, writerTask).Wait(); + + TestRunner.AssertTrue(readerStillHeld, + "Writer should not acquire the lock while a reader holds it"); + TestRunner.AssertTrue(writerRan, + "Writer should acquire after all readers release"); + }); + + // ── TC-RW-05 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-05: New readers are blocked while a writer is active", + () => + { + string name = $"rwl-tc05-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + bool readerAcquiredWhileWriterHeld = false; + + var writerReady = new ManualResetEventSlim(false); + var releaseWriter = new ManualResetEventSlim(false); + + var writerTask = Task.Run(() => + { + using var h = rwl.AcquireWriteLock(TimeSpan.FromSeconds(10)); + writerReady.Set(); + releaseWriter.Wait(); + h.Dispose(); + }); + + writerReady.Wait(); + + var readerTask = Task.Run(() => + { + using var h = rwl.AcquireReadLock(TimeSpan.FromSeconds(10)); + readerAcquiredWhileWriterHeld = true; + }); + + Thread.Sleep(500); + bool readerBlockedCorrectly = !readerAcquiredWhileWriterHeld; + releaseWriter.Set(); + + Task.WhenAll(writerTask, readerTask).Wait(); + + TestRunner.AssertTrue(readerBlockedCorrectly, + "Reader should not acquire the lock while a writer is active"); + TestRunner.AssertTrue(readerAcquiredWhileWriterHeld, + "Reader should succeed once the writer releases"); + }); + + // ── TC-RW-06 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-06: Two writers are mutually exclusive", + () => + { + string name = $"rwl-tc06-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + int concurrentWriters = 0; + bool exclusivityViolated = false; + + var w1Ready = new ManualResetEventSlim(false); + var releaseW1 = new ManualResetEventSlim(false); + + var w1 = Task.Run(() => + { + using var h = rwl.AcquireWriteLock(TimeSpan.FromSeconds(10)); + Interlocked.Increment(ref concurrentWriters); + w1Ready.Set(); + releaseW1.Wait(); + Interlocked.Decrement(ref concurrentWriters); + }); + + w1Ready.Wait(); + + var w2 = Task.Run(() => + { + using var h = rwl.AcquireWriteLock(TimeSpan.FromSeconds(10)); + int val = Interlocked.Increment(ref concurrentWriters); + if (val > 1) exclusivityViolated = true; + Interlocked.Decrement(ref concurrentWriters); + }); + + Thread.Sleep(500); + releaseW1.Set(); + + Task.WhenAll(w1, w2).Wait(); + + TestRunner.AssertFalse(exclusivityViolated, + "Two writers must never hold the lock simultaneously"); + }); + + // ── TC-RW-07 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-07: Read lock is released on Dispose — subsequent read lock succeeds", + () => + { + string name = $"rwl-tc07-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + var h1 = rwl.AcquireReadLock(TimeSpan.FromSeconds(5)); + h1.Dispose(); + + bool acquired = false; + var t = Task.Run(() => + { + using var h2 = rwl.AcquireReadLock(TimeSpan.FromSeconds(5)); + acquired = true; + }); + + bool done = t.Wait(TimeSpan.FromSeconds(6)); + TestRunner.AssertTrue(done, "Read acquire should complete quickly after Dispose"); + TestRunner.AssertTrue(acquired, "Second read acquire should succeed"); + }); + + // ── TC-RW-08 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-08: Write lock acquire throws TimeoutException when readers block", + () => + { + string name = $"rwl-tc08-{Guid.NewGuid()}"; + var rwl = provider.CreateReaderWriterLock(name); + + var release = new ManualResetEventSlim(false); + + var reader = Task.Run(() => + { + using var h = rwl.AcquireReadLock(TimeSpan.FromSeconds(15)); + release.Wait(); + }); + + Thread.Sleep(200); + + try + { + TestRunner.AssertThrows( + () => rwl.AcquireWriteLock(TimeSpan.FromMilliseconds(500)), + "Write lock should time out when a reader is active"); + } + finally + { + release.Set(); + reader.Wait(); + } + }); + + // ── TC-RW-09 ───────────────────────────────────────────────────── + TestRunner.Run("TC-RW-09: Two independent named locks do not interfere", + () => + { + var rwlA = provider.CreateReaderWriterLock($"rwl-tc09-A-{Guid.NewGuid()}"); + var rwlB = provider.CreateReaderWriterLock($"rwl-tc09-B-{Guid.NewGuid()}"); + + using var hA = rwlA.AcquireWriteLock(TimeSpan.FromSeconds(5)); + using var hB = rwlB.AcquireWriteLock(TimeSpan.FromSeconds(5)); + + TestRunner.AssertNotNull(hA, "Lock A write handle should be valid"); + TestRunner.AssertNotNull(hB, "Lock B write handle should be valid independently"); + }); + } + } +} diff --git a/NCache.OSS.DistributedLock/Tests/SemaphoreLockTesting.cs b/NCache.OSS.DistributedLock/Tests/SemaphoreLockTesting.cs new file mode 100644 index 0000000..ed45a86 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/SemaphoreLockTesting.cs @@ -0,0 +1,241 @@ +using Alachisoft.NCache.Client; +using NCache.DistributedLock.Providers; +using Medallion.Threading; + +namespace IDistribtuedLockTest +{ + /// + /// Integration tests for the distributed semaphore (NCacheDistributedSemaphore). + /// Validates capacity enforcement, concurrent slot management, release behaviour, + /// timeout handling, and dangling-lock cleanup. + /// + internal class SemaphoreLockTesting + { + public static void RunTests(ICache cache) + { + Console.WriteLine(); + Console.WriteLine("══ Semaphore Tests ══════════════════════════════════════════"); + + var provider = new NCacheDistributedSynchronizationProvider(cache); + + // ── TC-SM-01 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-01: Acquire up to maxCount slots — all succeed", + () => + { + int maxCount = 3; + string name = $"sem-tc01-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, maxCount); + + var handles = new List(); + try + { + for (int i = 0; i < maxCount; i++) + { + var h = sem.Acquire(TimeSpan.FromSeconds(5)); + TestRunner.AssertNotNull(h, $"Slot {i + 1} of {maxCount} should be acquired"); + handles.Add(h); + } + } + finally + { + foreach (var h in handles) h.Dispose(); + } + }); + + // ── TC-SM-02 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-02: Exceeding maxCount blocks until a slot is freed", + () => + { + int maxCount = 2; + string name = $"sem-tc02-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, maxCount); + + bool thirdAcquired = false; + bool firstTwoStillHeld = false; + + var slotReady = new CountdownEvent(maxCount); + var releaseTrigger = new ManualResetEventSlim(false); + + // Fill all slots + var holders = Enumerable.Range(0, maxCount).Select(_ => Task.Run(() => + { + using var h = sem.Acquire(TimeSpan.FromSeconds(10)); + slotReady.Signal(); + releaseTrigger.Wait(); + })).ToList(); + + slotReady.Wait(); // all slots taken + + // Third acquire waits for a slot + var waiter = Task.Run(() => + { + using var h = sem.Acquire(TimeSpan.FromSeconds(10)); + thirdAcquired = true; + }); + + Thread.Sleep(500); // waiter should still be blocked + firstTwoStillHeld = !thirdAcquired; + + releaseTrigger.Set(); // release holders + Task.WhenAll(holders).Wait(); + waiter.Wait(TimeSpan.FromSeconds(10)); + + TestRunner.AssertTrue(firstTwoStillHeld, + "Third acquire should be blocked while all slots are taken"); + TestRunner.AssertTrue(thirdAcquired, + "Third acquire should succeed once a slot is released"); + }); + + // ── TC-SM-03 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-03: Slot is freed on Dispose — subsequent acquire succeeds", + () => + { + string name = $"sem-tc03-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, 1); + + var h1 = sem.Acquire(TimeSpan.FromSeconds(5)); + h1.Dispose(); + + bool acquired = false; + var t = Task.Run(() => + { + using var h2 = sem.Acquire(TimeSpan.FromSeconds(3)); + acquired = true; + }); + + bool done = t.Wait(TimeSpan.FromSeconds(5)); + TestRunner.AssertTrue(done, "Second acquire should complete quickly after dispose"); + TestRunner.AssertTrue(acquired, "Second acquire should succeed"); + }); + + // ── TC-SM-04 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-04: Timeout throws when all slots are occupied", + () => + { + string name = $"sem-tc04-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, 1); + + var release = new ManualResetEventSlim(false); + + var holder = Task.Run(() => + { + using var h = sem.Acquire(TimeSpan.FromSeconds(15)); + release.Wait(); + }); + + Thread.Sleep(200); + + try + { + TestRunner.AssertThrows( + () => sem.Acquire(TimeSpan.FromMilliseconds(500)), + "Should throw TimeoutException when no slot available in time"); + } + finally + { + release.Set(); + holder.Wait(); + } + }); + + // ── TC-SM-05 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-05: MaxCount property matches constructor argument", + () => + { + string name = $"sem-tc05-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, 5); + TestRunner.AssertEqual(5, sem.MaxCount, + "MaxCount should equal the value passed to the constructor"); + }); + + // ── TC-SM-06 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-06: Concurrent acquires do not exceed maxCount simultaneously", + () => + { + int maxCount = 3; + string name = $"sem-tc06-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, maxCount); + + int current = 0; + bool capacityViolated = false; + var tasks = new List(); + + for (int i = 0; i < 6; i++) + { + tasks.Add(Task.Run(() => + { + using var h = sem.Acquire(TimeSpan.FromSeconds(20)); + int val = Interlocked.Increment(ref current); + if (val > maxCount) capacityViolated = true; + Thread.Sleep(300); + Interlocked.Decrement(ref current); + })); + } + + Task.WhenAll(tasks).Wait(); + + TestRunner.AssertFalse(capacityViolated, + "Concurrent holders should never exceed maxCount"); + }); + + // ── TC-SM-07 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-07: Slots are fully reusable after all are released", + () => + { + int maxCount = 2; + string name = $"sem-tc07-{Guid.NewGuid()}"; + var sem = provider.CreateSemaphore(name, maxCount); + + // Round 1 + var h1 = sem.Acquire(TimeSpan.FromSeconds(5)); + var h2 = sem.Acquire(TimeSpan.FromSeconds(5)); + h1.Dispose(); + h2.Dispose(); + + // Round 2 — same slots should be available again + bool round2OK = false; + var h3 = sem.Acquire(TimeSpan.FromSeconds(5)); + var h4 = sem.Acquire(TimeSpan.FromSeconds(5)); + round2OK = true; + h3.Dispose(); + h4.Dispose(); + + TestRunner.AssertTrue(round2OK, + "All semaphore slots should be reusable after release"); + }); + + // ── TC-SM-08 ───────────────────────────────────────────────────── + TestRunner.Run("TC-SM-08: Two semaphores with different names are independent", + () => + { + string nameA = $"sem-tc08-A-{Guid.NewGuid()}"; + string nameB = $"sem-tc08-B-{Guid.NewGuid()}"; + + var semA = provider.CreateSemaphore(nameA, 1); + var semB = provider.CreateSemaphore(nameB, 1); + + using var hA = semA.Acquire(TimeSpan.FromSeconds(5)); + using var hB = semB.Acquire(TimeSpan.FromSeconds(5)); + + TestRunner.AssertNotNull(hA, "Semaphore A should be acquirable"); + TestRunner.AssertNotNull(hB, "Semaphore B should be acquirable independently"); + }); + + TestRunner.Run("TC-SM-09: Epired semaphore", + () => + { + string name = $"sem-tc08-A-{Guid.NewGuid()}"; + + var sem = provider.CreateSemaphore(name, 1); + + using var hA = sem.Acquire(TimeSpan.FromSeconds(1)); + + Thread.Sleep(TimeSpan.FromSeconds(30)); + + using var hB = sem.Acquire(TimeSpan.FromSeconds(5)); + + TestRunner.AssertNotNull(hB, "Semaphore B should be acquired"); + }); + } + } +} diff --git a/NCache.OSS.DistributedLock/Tests/TestRunner.cs b/NCache.OSS.DistributedLock/Tests/TestRunner.cs new file mode 100644 index 0000000..e1254d2 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/TestRunner.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; + +namespace IDistribtuedLockTest +{ + /// + /// Lightweight test runner — no external test framework required. + /// Collects pass / fail / skip results and prints a summary. + /// + internal static class TestRunner + { + private static int _passed; + private static int _failed; + private static int _skipped; + private static readonly List _failures = new(); + + // ── Assertion helpers ──────────────────────────────────────────────── + + public static void AssertTrue(bool condition, string message) + { + if (!condition) + throw new AssertionException($"Expected TRUE — {message}"); + } + + public static void AssertFalse(bool condition, string message) + { + if (condition) + throw new AssertionException($"Expected FALSE — {message}"); + } + + public static void AssertNull(object? obj, string message) + { + if (obj != null) + throw new AssertionException($"Expected NULL — {message}"); + } + + public static void AssertNotNull(object? obj, string message) + { + if (obj == null) + throw new AssertionException($"Expected NOT NULL — {message}"); + } + + public static void AssertEqual(T expected, T actual, string message) + { + if (!EqualityComparer.Default.Equals(expected, actual)) + throw new AssertionException($"Expected [{expected}] but got [{actual}] — {message}"); + } + + public static void AssertThrows(Action action, string message) where TEx : Exception + { + try + { + action(); + throw new AssertionException($"Expected {typeof(TEx).Name} to be thrown — {message}"); + } + catch (TEx) { /* expected */ } + catch (AssertionException) { throw; } + catch (Exception ex) + { + throw new AssertionException( + $"Expected {typeof(TEx).Name} but got {ex.GetType().Name} — {message}"); + } + } + + // ── Test registration ──────────────────────────────────────────────── + + /// Run a named test case, catching and recording any failure. + public static void Run(string name, Action test) + { + Console.Write($" [{name}] ... "); + try + { + test(); + _passed++; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("PASS"); + } + catch (SkipException ex) + { + _skipped++; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"SKIP ({ex.Message})"); + } + catch (Exception ex) + { + _failed++; + _failures.Add($" • {name}: {ex.Message}"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"FAIL — {ex.Message}"); + } + finally + { + Console.ResetColor(); + } + } + + /// Skip the current test with a reason. + public static void Skip(string reason) => throw new SkipException(reason); + + // ── Summary ────────────────────────────────────────────────────────── + + public static void PrintSummary() + { + int total = _passed + _failed + _skipped; + Console.WriteLine(); + Console.WriteLine(new string('═', 60)); + Console.WriteLine($" Results: {total} tests | " + + $"Passed: {_passed} | Failed: {_failed} | Skipped: {_skipped}"); + if (_failures.Count > 0) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(); + Console.WriteLine(" FAILURES:"); + foreach (var f in _failures) + Console.WriteLine(f); + Console.ResetColor(); + } + Console.WriteLine(new string('═', 60)); + } + + public static bool HasFailures => _failed > 0; + + // ── Internal exception types ───────────────────────────────────────── + + internal class AssertionException : Exception + { + public AssertionException(string msg) : base(msg) { } + } + + internal class SkipException : Exception + { + public SkipException(string msg) : base(msg) { } + } + } +} diff --git a/NCache.OSS.DistributedLock/Tests/client.ncconf b/NCache.OSS.DistributedLock/Tests/client.ncconf new file mode 100644 index 0000000..f8d8a49 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/client.ncconf @@ -0,0 +1,7 @@ + + + + + + + diff --git a/NCache.OSS.DistributedLock/Tests/config.ncconf b/NCache.OSS.DistributedLock/Tests/config.ncconf new file mode 100644 index 0000000..5d9f677 --- /dev/null +++ b/NCache.OSS.DistributedLock/Tests/config.ncconf @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file