Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayBatchSeamTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

184 lines
8.0 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters;
/// <summary>
/// The MxGateway adapter's batch subscribe seam and the alarm-stream union
/// filter. The plain-vs-supervisory advise choice itself lives inside
/// <c>RealMxGatewayClient</c> (it needs a live MXAccess session); its bounded-pipeline
/// mechanism is pinned separately by <see cref="BatchSeamPrimitiveTests"/>.
/// </summary>
[Collection("DataConnectionManagerActor")]
public class MxGatewayBatchSeamTests
{
private static MxGatewayDataConnection NewAdapter(FakeMxGatewayClient fake) =>
new(fake, NullLogger<MxGatewayDataConnection>.Instance);
private static Dictionary<string, string> Details() => new()
{
["Endpoint"] = "http://gw:5000",
["ApiKey"] = "key",
["ClientName"] = "client-a",
["WriteUserId"] = "0",
["ReadTimeoutMs"] = "2000",
};
/// <summary>Verifies that unsubscribing an old handle preserves routing for the new handle.</summary>
/// <param name="batch">Whether the batch subscribe path is used instead of the single-tag path.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task UnsubscribeOldHandle_PreservesNewHandleRouting(bool batch)
{
var fake = new FakeMxGatewayClient();
await using var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
var received = new List<object?>();
var old = await adapter.SubscribeAsync("A.x", (_, value) => received.Add(value.Value));
await adapter.SubscribeAsync("A.x", (_, value) => received.Add(value.Value));
if (batch) await adapter.UnsubscribeBatchAsync([old]);
else await adapter.UnsubscribeAsync(old);
await WaitUntilAsync(() => fake.OnUpdate != null);
fake.OnUpdate!(new MxValueUpdate("A.x", 7, QualityCode.Good, DateTimeOffset.UtcNow));
Assert.Equal([7], received);
}
private static async Task WaitUntilAsync(Func<bool> condition)
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2));
while (!condition()) await Task.Delay(10, timeout.Token);
}
/// <summary>Verifies that a batch subscribe issues exactly one bulk call per batch.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeBatch_IssuesExactlyOneBulkCallPerBatch()
{
var fake = new FakeMxGatewayClient();
var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
var results = await adapter.SubscribeBatchAsync(
["A.x", "A.y", "A.z"], (_, _) => { });
// ONE bulk RPC for the whole batch — replacing the historical AddItem + Advise
// pair PER TAG (6 RPCs for these three tags).
Assert.Single(fake.BulkSubscribeCalls);
Assert.Equal(3, fake.BulkSubscribeCalls[0].Count);
Assert.Equal(3, results.Count);
Assert.All(results, r => Assert.True(r.Success));
Assert.All(results, r => Assert.NotNull(r.SubscriptionId));
}
/// <summary>Verifies that a batch subscribe reports per-tag failures without failing the batch.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeBatch_ReportsPerTagFailuresWithoutFailingTheBatch()
{
var fake = new FakeMxGatewayClient();
fake.BulkSubscribeFailures.Add("A.y");
var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
var results = await adapter.SubscribeBatchAsync(["A.x", "A.y"], (_, _) => { });
Assert.True(results.Single(r => r.TagPath == "A.x").Success);
var failed = results.Single(r => r.TagPath == "A.y");
Assert.False(failed.Success);
Assert.Null(failed.SubscriptionId);
Assert.NotNull(failed.ErrorMessage);
}
/// <summary>Verifies that a batch unsubscribe releases every id in one bulk call.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeBatch_ReleasesEveryIdInOneBulkCall()
{
var fake = new FakeMxGatewayClient();
var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
var results = await adapter.SubscribeBatchAsync(["A.x", "A.y"], (_, _) => { });
await adapter.UnsubscribeBatchAsync(results.Select(r => r.SubscriptionId!).ToList());
Assert.Single(fake.BulkUnsubscribeCalls);
Assert.Equal(2, fake.BulkUnsubscribeCalls[0].Count);
}
/// <summary>Verifies that the alarm stream uses the longest common prefix and refreshes for every new source.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AlarmStream_UsesLongestCommonPrefix_AndRefreshesEveryNewSource()
{
var fake = new FakeMxGatewayClient();
var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
void NoTransition(NativeAlarmTransition _) { }
// First source → the stream opens scoped to it.
await adapter.SubscribeAlarmsAsync("Area1.Tank1", null, NoTransition);
await WaitForPrefixCountAsync(fake, 1);
Assert.Equal("Area1.Tank1", fake.AlarmStreamPrefixes[0]);
// A sibling under the same prefix widens the union → one restart on "Area1.Tank".
await adapter.SubscribeAlarmsAsync("Area1.Tank2", null, NoTransition);
await WaitForPrefixCountAsync(fake, 2);
Assert.Equal("Area1.Tank", fake.AlarmStreamPrefixes[1]);
// A covered new source still needs its standing-state snapshot.
await adapter.SubscribeAlarmsAsync("Area1.Tank3.Sub", null, NoTransition);
await WaitForPrefixCountAsync(fake, 3);
Assert.Equal("Area1.Tank", fake.AlarmStreamPrefixes[2]);
// A source outside the prefix widens it again — here down to gateway-wide.
await adapter.SubscribeAlarmsAsync("Zone9.Pump", null, NoTransition);
await WaitForPrefixCountAsync(fake, 4);
Assert.Null(fake.AlarmStreamPrefixes[3]); // "" → gateway-wide
}
/// <summary>Verifies that unsubscribing never restarts the alarm stream.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AlarmStream_UnsubscribeNeverRestartsTheStream()
{
var fake = new FakeMxGatewayClient();
var adapter = NewAdapter(fake);
await adapter.ConnectAsync(Details());
var first = await adapter.SubscribeAlarmsAsync("Area1.Tank1", null, _ => { });
// Wait for the first stream to actually open before widening: the open runs on a
// Task.Run whose token the restart cancels, so a restart racing an unstarted task
// would leave the first prefix unrecorded.
await WaitForPrefixCountAsync(fake, 1);
await adapter.SubscribeAlarmsAsync("Area1.Tank2", null, _ => { });
await WaitForPrefixCountAsync(fake, 2);
// Dropping a source leaves the prefix too BROAD at worst — bandwidth, not
// correctness — so no restart is issued.
await adapter.UnsubscribeAlarmsAsync(first);
await Task.Delay(100);
Assert.Equal(2, fake.AlarmStreamPrefixes.Count);
}
private static async Task WaitForPrefixCountAsync(FakeMxGatewayClient fake, int expected)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
while (DateTime.UtcNow < deadline)
{
lock (fake.AlarmStreamPrefixes)
{
if (fake.AlarmStreamPrefixes.Count >= expected)
return;
}
await Task.Delay(20);
}
Assert.Fail($"alarm stream was opened {fake.AlarmStreamPrefixes.Count} time(s), expected {expected}");
}
}