3fb6549209
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
113 lines
4.6 KiB
C#
113 lines
4.6 KiB
C#
using Moq;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests;
|
|
|
|
/// <summary>
|
|
/// Driver.OpcUaClient.Browser-002: when the caller cancels during the <c>BrowseNextAsync</c>
|
|
/// pagination loop, the server-side continuation point must be released
|
|
/// (<c>releaseContinuationPoints: true</c>) before the cancellation propagates. Mirrors the
|
|
/// runtime driver's <c>OpcUaClientContinuationPointReleaseTests</c>; no live server needed.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientBrowseSessionPaginationTests
|
|
{
|
|
private static readonly byte[] FakeContinuationPoint = [0x01, 0x02, 0x03];
|
|
|
|
private static Mock<ISession> BuildSession(bool cancelNext)
|
|
{
|
|
var mock = new Mock<ISession>(MockBehavior.Loose);
|
|
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext());
|
|
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
|
|
|
var firstRef = new ReferenceDescription
|
|
{
|
|
NodeId = new ExpandedNodeId(new NodeId(1000u)),
|
|
BrowseName = new QualifiedName("TestObject"),
|
|
DisplayName = new LocalizedText("TestObject"),
|
|
NodeClass = NodeClass.Object,
|
|
};
|
|
mock.Setup(s => s.BrowseAsync(
|
|
It.IsAny<RequestHeader>(), It.IsAny<ViewDescription>(), It.IsAny<uint>(),
|
|
It.IsAny<BrowseDescriptionCollection>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new BrowseResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results = new BrowseResultCollection
|
|
{
|
|
new BrowseResult
|
|
{
|
|
ContinuationPoint = FakeContinuationPoint,
|
|
References = new ReferenceDescriptionCollection { firstRef },
|
|
},
|
|
},
|
|
DiagnosticInfos = new DiagnosticInfoCollection(),
|
|
});
|
|
|
|
var emptyPage = new BrowseNextResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results = new BrowseResultCollection
|
|
{
|
|
new BrowseResult
|
|
{
|
|
ContinuationPoint = Array.Empty<byte>(),
|
|
References = new ReferenceDescriptionCollection(),
|
|
},
|
|
},
|
|
DiagnosticInfos = new DiagnosticInfoCollection(),
|
|
};
|
|
|
|
var pageSetup = mock.Setup(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(), It.Is<bool>(r => !r),
|
|
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()));
|
|
if (cancelNext) pageSetup.ThrowsAsync(new OperationCanceledException("cancelled mid-page"));
|
|
else pageSetup.ReturnsAsync(emptyPage);
|
|
|
|
mock.Setup(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(), It.Is<bool>(r => r),
|
|
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(emptyPage);
|
|
return mock;
|
|
}
|
|
|
|
private static OpcUaClientBrowseSession NewSession(Mock<ISession> mock)
|
|
=> new(mock.Object, NamespaceMap.FromTable(new NamespaceTable()), ObjectIds.ObjectsFolder);
|
|
|
|
/// <summary>Cancellation mid-pagination releases the continuation point exactly once.</summary>
|
|
[Fact]
|
|
public async Task RootAsync_releases_continuation_point_when_cancelled_mid_pagination()
|
|
{
|
|
var mock = BuildSession(cancelNext: true);
|
|
await using var session = NewSession(mock);
|
|
|
|
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
|
await session.RootAsync(TestContext.Current.CancellationToken));
|
|
|
|
mock.Verify(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(), It.Is<bool>(r => r),
|
|
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
/// <summary>Positive control: an uncancelled pagination never issues a release call.</summary>
|
|
[Fact]
|
|
public async Task RootAsync_does_not_release_when_pagination_completes()
|
|
{
|
|
var mock = BuildSession(cancelNext: false);
|
|
await using var session = NewSession(mock);
|
|
|
|
var nodes = await session.RootAsync(TestContext.Current.CancellationToken);
|
|
|
|
nodes.Count.ShouldBe(1);
|
|
mock.Verify(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(), It.Is<bool>(r => !r),
|
|
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
mock.Verify(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(), It.Is<bool>(r => r),
|
|
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()), Times.Never);
|
|
}
|
|
}
|