using System.Threading.Channels; using Google.Protobuf.WellKnownTypes; using MxGateway.Contracts.Proto; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests; /// /// Driver-level infrastructure tests for findings Driver.Galaxy-007 (dispose-CTS gate) /// and Driver.Galaxy-011 (GetMemoryFootprint non-zero estimate). /// public sealed class GalaxyDriverInfrastructureTests { private static GalaxyDriverOptions Opts() => new( new GalaxyGatewayOptions("https://mxgw.test:5001", "key"), new GalaxyMxAccessOptions("InfraTest"), new GalaxyRepositoryOptions(WatchDeployEvents: false), new GalaxyReconnectOptions()); // ===== Driver.Galaxy-011 regression: GetMemoryFootprint reflects registry size ===== [Fact] public void GetMemoryFootprint_IsZeroWhenNoSubscriptions() { var sub = new NoOpSubscriber(); using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub); driver.GetMemoryFootprint().ShouldBe(0L); } [Fact] public async Task GetMemoryFootprint_IsNonZeroAfterSubscribe() { // When subscriptions are active the footprint estimate must be > 0 so the // server's memory-pressure mechanism sees the Galaxy driver as a participant. var sub = new NoOpSubscriber(); using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub); await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None); driver.GetMemoryFootprint().ShouldBeGreaterThan(0L); } [Fact] public async Task GetMemoryFootprint_DecreasesAfterUnsubscribe() { var sub = new NoOpSubscriber(); using var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub); var handle = await driver.SubscribeAsync(["Tag.A", "Tag.B"], TimeSpan.Zero, CancellationToken.None); var afterSubscribe = driver.GetMemoryFootprint(); afterSubscribe.ShouldBeGreaterThan(0L); await driver.UnsubscribeAsync(handle, CancellationToken.None); var afterUnsubscribe = driver.GetMemoryFootprint(); afterUnsubscribe.ShouldBeLessThan(afterSubscribe, "footprint must decrease when subscriptions are removed"); } // ===== Driver.Galaxy-007 regression: Dispose cancels the dispose CTS ===== [Fact] public async Task Dispose_SetsDisposedFlag_BlockingFurtherCapabilityCalls() { var sub = new NoOpSubscriber(); var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub); driver.Dispose(); // Capability entry points all check ObjectDisposedException.ThrowIf — SubscribeAsync // is representative and is guarded on line 1 of its body. await Should.ThrowAsync(() => driver.SubscribeAsync(["Tag.A"], TimeSpan.Zero, CancellationToken.None)); } [Fact] public async Task DisposeAsync_CanBeAwaitedWithoutDeadlock() { var sub = new NoOpSubscriber(); var driver = new GalaxyDriver("drv-1", Opts(), null, null, null, sub); // IAsyncDisposable.DisposeAsync must not block or throw. await Should.NotThrowAsync(async () => await driver.DisposeAsync()); } // ===== Minimal IGalaxySubscriber fake that returns empty results for subscribe calls ===== private sealed class NoOpSubscriber : IGalaxySubscriber { private readonly Channel _stream = Channel.CreateUnbounded(); public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { var results = fullReferences.Select((r, i) => new SubscribeResult { TagAddress = r, ItemHandle = i + 1, WasSuccessful = true, }).ToList(); return Task.FromResult>(results); } public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken) => _stream.Reader.ReadAllAsync(cancellationToken); } }