using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Junction.Core.Polling; using Junction.Domain; using Junction.Domain.Models; using Junction.Domain.Protocols; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Junction.Tests.Unit { /// /// Behavior tests for using a hand-rolled fake driver. /// Timings are deliberately generous to avoid CI flake. /// public class PollingEngineTests { private static PollingEngine NewEngine() => new PollingEngine(NullLogger.Instance); private static Machine MachineWithInterval(TimeSpan interval) => new Machine(Guid.NewGuid(), "M1", "fake", null, interval); private static MachineSnapshot Snapshot(Guid machineId) => new MachineSnapshot( machineId, DateTimeOffset.UtcNow, ConnectionState.Connected, Array.Empty()); [Fact] public async Task RunAsync_PollsRepeatedly_OverShortInterval() { var machine = MachineWithInterval(TimeSpan.FromMilliseconds(25)); var driver = new FakeDriver(m => Result.Ok(Snapshot(machine.Id))); int count = 0; using var cts = new CancellationTokenSource(); var loop = NewEngine().RunAsync(machine, driver, _ => Interlocked.Increment(ref count), cts.Token); await Task.Delay(200); cts.Cancel(); await loop; Assert.True(count >= 2, $"expected >= 2 polls, got {count}"); } [Fact] public async Task RunAsync_Cancellation_StopsLoop_NoException() { var machine = MachineWithInterval(TimeSpan.FromMilliseconds(20)); var driver = new FakeDriver(m => Result.Ok(Snapshot(machine.Id))); using var cts = new CancellationTokenSource(); var loop = NewEngine().RunAsync(machine, driver, _ => { }, cts.Token); await Task.Delay(60); cts.Cancel(); // Must complete promptly and without propagating any exception. var completed = await Task.WhenAny(loop, Task.Delay(1000)) == loop; Assert.True(completed, "loop did not stop promptly after cancellation"); await loop; // would rethrow if it faulted Assert.True(loop.IsCompletedSuccessfully); } [Fact] public async Task RunAsync_DriverFail_DoesNotEmit_AndKeepsLooping() { var machine = MachineWithInterval(TimeSpan.FromMilliseconds(25)); int calls = 0; var emitted = new List(); // Fail on cycle 1, succeed thereafter. Proves the loop survives a fault. var driver = new FakeDriver(m => { int c = Interlocked.Increment(ref calls); if (c == 1) { return Result.Fail(OperationError.Of("fake", "boom")); } return Result.Ok(Snapshot(machine.Id)); }); using var cts = new CancellationTokenSource(); var loop = NewEngine().RunAsync(machine, driver, s => { lock (emitted) { emitted.Add(s); } }, cts.Token); await Task.Delay(200); cts.Cancel(); await loop; Assert.True(calls >= 2, $"expected loop to continue past the failed cycle, calls={calls}"); lock (emitted) { // First (failed) cycle emitted nothing; a later Ok cycle did. Assert.NotEmpty(emitted); Assert.True(emitted.Count < calls, "a failed cycle must not have emitted a snapshot"); } } [Fact] public async Task RunAsync_Ok_ForwardsSnapshotIntact() { var machine = MachineWithInterval(TimeSpan.FromMilliseconds(20)); var expected = Snapshot(machine.Id); var driver = new FakeDriver(m => Result.Ok(expected)); MachineSnapshot? received = null; using var cts = new CancellationTokenSource(); var loop = NewEngine().RunAsync(machine, driver, s => { received = s; cts.Cancel(); }, cts.Token); await loop; Assert.Same(expected, received); } [Fact] public async Task RunAsync_DriverReturnsCancelled_StopsLoop() { var machine = MachineWithInterval(TimeSpan.FromMilliseconds(20)); int calls = 0; var driver = new FakeDriver(m => { Interlocked.Increment(ref calls); return Result.Cancelled(); }); using var cts = new CancellationTokenSource(); var loop = NewEngine().RunAsync(machine, driver, _ => { }, cts.Token); var completed = await Task.WhenAny(loop, Task.Delay(1000)) == loop; Assert.True(completed, "loop did not stop on cancelled result"); await loop; Assert.Equal(1, calls); } /// Hand-rolled fake driver; behavior supplied by a delegate. private sealed class FakeDriver : IProtocolDriver { private readonly Func> _behavior; public FakeDriver(Func> behavior) { _behavior = behavior; } public string ProtocolId => "fake"; public Task> ReadCurrentAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(_behavior(null!)); } } } }