Skip to content

Commit 57f5d63

Browse files
committed
@
test(semaphore): cover heartbeat/acquisition timer callbacks Add two AutoStart SemaphoreService tests with short intervals so the heartbeat and acquisition timer callbacks actually fire: - heartbeat timer renews and keeps the slot held; - acquisition timer waits while full, then acquires once a slot frees. This covers the previously-untested timer-callback region. SemaphoreService line coverage 70.9% -> 83.2% (575 unit tests total). @
1 parent 8eb7559 commit 57f5d63

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

tests/MultiLock.Tests/SemaphoreServiceTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,74 @@ public async Task TryAcquireAsync_WhenProviderAlwaysThrows_RethrowsAfterExhausti
466466
Times.Exactly(3));
467467
}
468468

469+
[Fact]
470+
public async Task AutoStart_HeartbeatTimerFires_KeepsSlotHeld()
471+
{
472+
// Arrange: short heartbeat interval so the heartbeat timer callback actually fires.
473+
var options = new SemaphoreOptions
474+
{
475+
SemaphoreName = "heartbeat-semaphore",
476+
MaxCount = 1,
477+
AutoStart = true,
478+
HeartbeatInterval = TimeSpan.FromMilliseconds(50),
479+
HeartbeatTimeout = TimeSpan.FromMilliseconds(250),
480+
AcquisitionInterval = TimeSpan.FromMilliseconds(50),
481+
EnableDetailedLogging = true
482+
};
483+
service = new SemaphoreService(provider, Options.Create(options), loggerFactory.CreateLogger<SemaphoreService>());
484+
485+
// Act: AutoStart acquires immediately, then the heartbeat timer renews the slot repeatedly.
486+
await service.StartAsync();
487+
await WaitUntilAsync(() => service.IsHolding, TimeSpan.FromSeconds(5));
488+
489+
// Assert: after several heartbeat intervals the slot is still held (renewals succeeded).
490+
await Task.Delay(250);
491+
service.IsHolding.ShouldBeTrue();
492+
}
493+
494+
[Fact]
495+
public async Task AutoStart_WhenInitiallyFull_AcquisitionTimerAcquiresOnceSlotFrees()
496+
{
497+
// Arrange: occupy the only slot, then start an auto-starting service that must wait.
498+
const string semaphore = "acquisition-semaphore";
499+
var meta = new Dictionary<string, string>();
500+
await provider.TryAcquireAsync(semaphore, "other-holder", 1, meta, TimeSpan.FromMinutes(5));
501+
502+
var options = new SemaphoreOptions
503+
{
504+
SemaphoreName = semaphore,
505+
MaxCount = 1,
506+
AutoStart = true,
507+
HeartbeatInterval = TimeSpan.FromMilliseconds(50),
508+
HeartbeatTimeout = TimeSpan.FromMilliseconds(250),
509+
AcquisitionInterval = TimeSpan.FromMilliseconds(50)
510+
};
511+
service = new SemaphoreService(provider, Options.Create(options), loggerFactory.CreateLogger<SemaphoreService>());
512+
513+
// Act: the acquisition timer keeps retrying while the semaphore is full.
514+
await service.StartAsync();
515+
await Task.Delay(150);
516+
service.IsHolding.ShouldBeFalse();
517+
518+
// Free the slot; the acquisition timer should pick it up on a subsequent tick.
519+
await provider.ReleaseAsync(semaphore, "other-holder");
520+
521+
// Assert
522+
await WaitUntilAsync(() => service.IsHolding, TimeSpan.FromSeconds(5));
523+
service.IsHolding.ShouldBeTrue();
524+
}
525+
526+
private static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
527+
{
528+
using var cts = new CancellationTokenSource(timeout);
529+
while (!condition())
530+
{
531+
if (cts.IsCancellationRequested)
532+
throw new TimeoutException("Condition was not met within the allotted time.");
533+
await Task.Delay(20);
534+
}
535+
}
536+
469537
private SemaphoreService CreateService(string semaphoreName, int maxCount) =>
470538
new(provider, Options.Create(new SemaphoreOptions
471539
{

0 commit comments

Comments
 (0)