using System.Runtime.CompilerServices; using Shouldly; using Xunit; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// #865: — what advise paths wait on so an initial value is /// never pushed before the stream is requested. It must complete once the RPC is started (before any /// event), and must never strand a waiter on a pump whose loop died before getting that far. /// public sealed class EventPumpStreamRequestedTests { private sealed class ScriptedSubscriber(Func> stream) : IGalaxySubscriber { public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) => Task.FromResult>([]); public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken) => stream(cancellationToken); } /// A stream that is requested and then never yields: StreamRequested completes; the RPC started first. [Fact] public async Task Completes_once_the_stream_is_requested_before_any_event() { var rpcStarted = 0; async IAsyncEnumerable Never([EnumeratorCancellation] CancellationToken ct) { Interlocked.Exchange(ref rpcStarted, 1); // the synchronous first step — where a real call is created await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false); yield break; } await using var pump = new EventPump(new ScriptedSubscriber(Never), new SubscriptionRegistry()); pump.StreamRequested.IsCompleted.ShouldBeFalse("not before Start"); pump.Start(); await TestWait.ForAsync(pump.StreamRequested, "StreamRequested must complete once the stream call is started"); Volatile.Read(ref rpcStarted).ShouldBe(1, "StreamRequested must not precede the stream call"); } /// A subscriber that throws before reaching the RPC still completes StreamRequested (no stranded waiter). [Fact] public async Task Completes_when_the_stream_fails_before_the_rpc() { var faulted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var pump = new EventPump( new ScriptedSubscriber(_ => throw new InvalidOperationException("not connected")), new SubscriptionRegistry(), onStreamFault: ex => faulted.TrySetResult(ex)); pump.Start(); await TestWait.ForAsync(pump.StreamRequested, "a dead pump must not strand a StreamRequested waiter"); (await TestWait.ForAsync(faulted.Task, "positive control: the failure is still reported as a stream fault")) .ShouldBeOfType(); } }