Files
adas-core/adas-core.Test/Services/InMemoryLockProviderTest.cs
2026-06-26 10:29:23 +02:00

146 lines
5.7 KiB
C#

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!;
/// <summary>
/// Initializes a fresh <see cref="InMemoryLockProvider"/> instance for the test fixture, ensuring each test starts with a clean, isolated provider state.
/// </summary>
[SetUp]
public void SetUp()
{
_provider = new InMemoryLockProvider();
}
/// <summary>
/// Retrieves the private <c>_locks</c> dictionary from the associated <see cref="InMemoryLockProvider"/> instance using reflection, exposing the underlying locks for inspection or manipulation in tests.
/// </summary>
/// <returns>The <see cref="ConcurrentDictionary{TKey, TValue}"/> mapping lock keys to their <see cref="SemaphoreSlim"/> instances held by the provider.</returns>
private ConcurrentDictionary<string, SemaphoreSlim> GetLocks()
{
var field = typeof(InMemoryLockProvider)
.GetField("_locks", BindingFlags.NonPublic | BindingFlags.Instance);
return (ConcurrentDictionary<string, SemaphoreSlim>)field!.GetValue(_provider)!;
}
#region TC-50
/// <summary>
/// Verifies that AcquireAsync returns <c>true</c> and creates a corresponding entry in the locks dictionary when called with a valid key and time span.
/// </summary>
[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
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// Verifies that calling <c>ReleaseAsync</c> on a semaphore provider restores availability,
/// allowing a subsequent <c>AcquireAsync</c> for the same key to succeed and the semaphore's
/// <c>CurrentCount</c> to transition back to its released value.
/// </summary>
[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
/// <summary>
/// Verifies that <c>ReleaseAsync</c> does not throw when invoked without a prior <c>AcquireAsync</c> for the given key, and remains safe to call multiple times after a single successful acquire.
/// </summary>
[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
/// <summary>
/// Verifies that <c>AcquireAsync</c> 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.
/// </summary>
[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
}