Files
scadalink-design/tests/ScadaLink.DeploymentManager.Tests/OperationLockManagerTests.cs
Joseph Doherty 6ea38faa6f Phase 3C: Deployment pipeline & Store-and-Forward engine
Deployment Manager (WP-1–8, WP-16):
- DeploymentService: full pipeline (flatten→validate→send→track→audit)
- OperationLockManager: per-instance concurrency control
- StateTransitionValidator: Enabled/Disabled/NotDeployed transition matrix
- ArtifactDeploymentService: broadcast to all sites with per-site results
- Deployment identity (GUID + revision hash), idempotency, staleness detection
- Instance lifecycle commands (disable/enable/delete) with deduplication

Store-and-Forward (WP-9–15):
- StoreAndForwardStorage: SQLite persistence, 3 categories, no max buffer
- StoreAndForwardService: fixed-interval retry, transient-only buffering, parking
- ReplicationService: async best-effort to standby (fire-and-forget)
- Parked message management (query/retry/discard from central)
- Messages survive instance deletion, S&F drains on disable

620 tests pass (+79 new), zero warnings.
2026-03-16 21:27:18 -04:00

96 lines
3.0 KiB
C#

namespace ScadaLink.DeploymentManager.Tests;
/// <summary>
/// WP-3: Tests for per-instance operation lock.
/// </summary>
public class OperationLockManagerTests
{
private readonly OperationLockManager _lockManager = new();
[Fact]
public async Task AcquireAsync_ReturnsDisposable()
{
using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
Assert.NotNull(lockHandle);
}
[Fact]
public async Task AcquireAsync_SameInstance_BlocksSecondCaller()
{
using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
// Second acquire should time out
await Assert.ThrowsAsync<TimeoutException>(() =>
_lockManager.AcquireAsync("inst1", TimeSpan.FromMilliseconds(50)));
}
[Fact]
public async Task AcquireAsync_DifferentInstances_BothSucceed()
{
using var lock1 = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
using var lock2 = await _lockManager.AcquireAsync("inst2", TimeSpan.FromSeconds(5));
Assert.NotNull(lock1);
Assert.NotNull(lock2);
}
[Fact]
public async Task AcquireAsync_AfterRelease_CanReacquire()
{
var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
firstLock.Dispose();
// Should succeed now
using var secondLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
Assert.NotNull(secondLock);
}
[Fact]
public async Task IsLocked_ReturnsTrueWhileLocked()
{
Assert.False(_lockManager.IsLocked("inst1"));
using var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
Assert.True(_lockManager.IsLocked("inst1"));
}
[Fact]
public async Task IsLocked_ReturnsFalseAfterRelease()
{
var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
lockHandle.Dispose();
Assert.False(_lockManager.IsLocked("inst1"));
}
[Fact]
public async Task AcquireAsync_DoubleDispose_DoesNotThrow()
{
var lockHandle = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(5));
lockHandle.Dispose();
lockHandle.Dispose(); // Should not throw
}
[Fact]
public async Task AcquireAsync_CancellationToken_Respected()
{
using var firstLock = await _lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30));
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
_lockManager.AcquireAsync("inst1", TimeSpan.FromSeconds(30), cts.Token));
}
[Fact]
public async Task AcquireAsync_ConcurrentDifferentInstances_AllSucceed()
{
var tasks = Enumerable.Range(0, 10).Select(async i =>
{
using var lockHandle = await _lockManager.AcquireAsync($"inst{i}", TimeSpan.FromSeconds(5));
await Task.Delay(10);
});
await Task.WhenAll(tasks);
}
}