using System.Collections.Concurrent;
using System.Reflection;
using adas_core.Application.Services.Caching;
namespace adas_core.Test.Services;
[TestFixture]
public class InMemoryLockProviderTest
{
private InMemoryLockProvider _provider = null!;
///
/// Initializes a fresh instance for the test fixture, ensuring each test starts with a clean, isolated provider state.
///
[SetUp]
public void SetUp()
{
_provider = new InMemoryLockProvider();
}
///
/// Retrieves the private _locks dictionary from the associated instance using reflection, exposing the underlying locks for inspection or manipulation in tests.
///
/// The mapping lock keys to their instances held by the provider.
private ConcurrentDictionary GetLocks()
{
var field = typeof(InMemoryLockProvider)
.GetField("_locks", BindingFlags.NonPublic | BindingFlags.Instance);
return (ConcurrentDictionary)field!.GetValue(_provider)!;
}
#region TC-50
///
/// Verifies that AcquireAsync returns true and creates a corresponding entry in the locks dictionary when called with a valid key and time span.
///
[Test]
public async Task AcquireAsync_ReturnsTrue_AndCreatesEntryInLocksDictionary()
{
var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
Assert.That(result, Is.True);
var locks = GetLocks();
Assert.That(locks.ContainsKey("key"), Is.True);
}
#endregion
#region TC-51
///
/// Verifies that AcquireAsync returns false when the semaphore for the specified key is already occupied and the requested timeout expires before the lock can be acquired, and that the underlying semaphore is restored to a count of 1 after release.
///
[Test]
public async Task AcquireAsync_ReturnsFalse_WhenSemaphoreOccupiedAndTimeoutExpires()
{
await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(100));
Assert.That(result, Is.False);
await _provider.ReleaseAsync("key");
var locks = GetLocks();
Assert.That(locks["key"].CurrentCount, Is.EqualTo(1));
}
#endregion
#region TC-52
///
/// Verifies that calling ReleaseAsync on a semaphore provider restores availability,
/// allowing a subsequent AcquireAsync for the same key to succeed and the semaphore's
/// CurrentCount to transition back to its released value.
///
[Test]
public async Task ReleaseAsync_MakesSemaphoreAvailableForNextAcquire()
{
await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
await _provider.ReleaseAsync("key");
var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
Assert.That(result, Is.True);
var locks = GetLocks();
Assert.That(locks["key"].CurrentCount, Is.EqualTo(0));
await _provider.ReleaseAsync("key");
Assert.That(locks["key"].CurrentCount, Is.EqualTo(1));
}
#endregion
#region TC-53
///
/// Verifies that ReleaseAsync does not throw when invoked without a prior AcquireAsync for the given key, and remains safe to call multiple times after a single successful acquire.
///
[Test]
public async Task ReleaseAsync_DoesNotThrow_WhenCalledWithoutPriorAcquire()
{
await _provider.ReleaseAsync("nonexistent-key");
await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
await _provider.ReleaseAsync("key");
await _provider.ReleaseAsync("key");
}
#endregion
#region TC-54
///
/// Verifies that AcquireAsync is thread-safe for a given key, ensuring that only one caller can hold the semaphore at a time even when multiple tasks compete concurrently for the same key, and that a single semaphore instance is created and reused per key.
///
[Test]
public async Task AcquireAsync_IsThreadSafe_OnlyOneHolderAtATime_SingleSemaphorePerKey()
{
const string key = "same-key";
const int threadCount = 10;
var acquiredCount = 0;
var currentHolders = 0;
var tasks = Enumerable.Range(0, threadCount).Select(_ => Task.Run(async () =>
{
var acquired = await _provider.AcquireAsync(key, TimeSpan.FromSeconds(10));
Assert.That(acquired, Is.True);
var current = Interlocked.Increment(ref currentHolders);
Assert.That(current, Is.EqualTo(1));
Interlocked.Increment(ref acquiredCount);
await Task.Delay(5);
Interlocked.Decrement(ref currentHolders);
await _provider.ReleaseAsync(key);
})).ToArray();
await Task.WhenAll(tasks);
Assert.That(acquiredCount, Is.EqualTo(threadCount));
var locks = GetLocks();
Assert.That(locks.ContainsKey(key), Is.True);
Assert.That(locks.Keys.Count(k => k == key), Is.EqualTo(1));
}
#endregion
}