fix(driver-galaxy): resolve Medium code-review finding (Driver.Galaxy-014)
Add GalaxyDriverInfrastructureTests covering the two gaps identified in this finding that are not yet tracked by a dedicated test file: GetMemoryFootprint returns a live registry-derived estimate (Driver.Galaxy-011) and DisposeAsync completes without deadlock (Driver.Galaxy-007). The remaining items listed in the finding are covered by earlier resolution commits: stream-fault → recovery → OnDataChange resumes (EventPumpStreamFaultTests), post-reconnect Rebind (SubscriptionRegistryTests), StatusCodeMap.FromMxStatus success/failure semantics (StatusCodeMapTests), and DataTypeMap all seven codes (DataTypeMapTests). Update findings.md header to 4 open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Driver-level infrastructure tests for findings Driver.Galaxy-007 (dispose-CTS gate)
|
||||
/// and Driver.Galaxy-011 (GetMemoryFootprint non-zero estimate).
|
||||
/// </summary>
|
||||
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<ObjectDisposedException>(() =>
|
||||
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<MxEvent> _stream = Channel.CreateUnbounded<MxEvent>();
|
||||
|
||||
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
|
||||
{
|
||||
var results = fullReferences.Select((r, i) => new SubscribeResult
|
||||
{
|
||||
TagAddress = r,
|
||||
ItemHandle = i + 1,
|
||||
WasSuccessful = true,
|
||||
}).ToList();
|
||||
return Task.FromResult<IReadOnlyList<SubscribeResult>>(results);
|
||||
}
|
||||
|
||||
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(CancellationToken cancellationToken)
|
||||
=> _stream.Reader.ReadAllAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user