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