test: verify SDK 2 driver boundaries and lifecycle contracts
This commit is contained in:
+10
-10
@@ -20,23 +20,23 @@ public sealed class OpcUaClientBrowseSessionAttributesTests
|
||||
private static Mock<ISession> SessionReturning(NodeClass nodeClass, NodeId dataType, int valueRank, byte userAccess)
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(BrowseTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
mock.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ArrayOf<ReadValueId> nodes, CancellationToken _) =>
|
||||
new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select(n => n.AttributeId switch
|
||||
Results = [.. nodes.ToList().Select(n => n.AttributeId switch
|
||||
{
|
||||
Attributes.NodeClass => new DataValue(new Variant((int)nodeClass)),
|
||||
Attributes.DisplayName => new DataValue(new Variant(new LocalizedText("Speed"))),
|
||||
Attributes.DataType => new DataValue(new Variant(dataType)),
|
||||
Attributes.ValueRank => new DataValue(new Variant(valueRank)),
|
||||
Attributes.UserAccessLevel => new DataValue(new Variant(userAccess)),
|
||||
_ => new DataValue(StatusCodes.BadAttributeIdInvalid),
|
||||
_ => DataValue.FromStatusCode(StatusCodes.BadAttributeIdInvalid),
|
||||
})],
|
||||
DiagnosticInfos = [],
|
||||
});
|
||||
@@ -101,19 +101,19 @@ public sealed class OpcUaClientBrowseSessionAttributesTests
|
||||
session.AttributesAsync("nsu=urn:not-in-table;s=X", TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
private sealed class TrackingCredential : IDisposable
|
||||
private sealed class TrackingApplication : IAsyncDisposable
|
||||
{
|
||||
public int Disposed { get; private set; }
|
||||
|
||||
public void Dispose() => Disposed++;
|
||||
public ValueTask DisposeAsync() { Disposed++; return ValueTask.CompletedTask; }
|
||||
}
|
||||
|
||||
/// <summary>Review m9: the browse session disposes the user certificate it was handed, with itself.</summary>
|
||||
/// <summary>Review m9: the browse session disposes the SDK application it was handed, with itself.</summary>
|
||||
[Fact]
|
||||
public async Task The_session_disposes_the_credential_it_owns()
|
||||
public async Task The_session_disposes_the_application_it_owns()
|
||||
{
|
||||
var mock = SessionReturning(NodeClass.Variable, DataTypeIds.Int32, -1, 1);
|
||||
var credential = new TrackingCredential();
|
||||
var credential = new TrackingApplication();
|
||||
var session = new OpcUaClientBrowseSession(
|
||||
mock.Object, NamespaceMap.FromTable(new NamespaceTable()), ObjectIds.ObjectsFolder, credential);
|
||||
|
||||
|
||||
+15
-15
@@ -20,7 +20,7 @@ public sealed class OpcUaClientBrowseSessionPaginationTests
|
||||
private static Mock<ISession> BuildSession(bool cancelNext)
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(BrowseTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
|
||||
var firstRef = new ReferenceDescription
|
||||
@@ -32,44 +32,44 @@ public sealed class OpcUaClientBrowseSessionPaginationTests
|
||||
};
|
||||
mock.Setup(s => s.BrowseAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<ViewDescription>(), It.IsAny<uint>(),
|
||||
It.IsAny<BrowseDescriptionCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<BrowseDescription>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BrowseResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new BrowseResultCollection
|
||||
Results = new List<BrowseResult>
|
||||
{
|
||||
new BrowseResult
|
||||
{
|
||||
ContinuationPoint = FakeContinuationPoint,
|
||||
References = new ReferenceDescriptionCollection { firstRef },
|
||||
ContinuationPoint = FakeContinuationPoint.ToByteString(),
|
||||
References = new List<ReferenceDescription> { firstRef },
|
||||
},
|
||||
},
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
});
|
||||
|
||||
var emptyPage = new BrowseNextResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new BrowseResultCollection
|
||||
Results = new List<BrowseResult>
|
||||
{
|
||||
new BrowseResult
|
||||
{
|
||||
ContinuationPoint = Array.Empty<byte>(),
|
||||
References = new ReferenceDescriptionCollection(),
|
||||
ContinuationPoint = ByteString.Empty,
|
||||
References = new List<ReferenceDescription>(),
|
||||
},
|
||||
},
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
};
|
||||
|
||||
var pageSetup = mock.Setup(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(), It.Is<bool>(r => !r),
|
||||
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()));
|
||||
It.IsAny<ArrayOf<ByteString>>(), 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>()))
|
||||
It.IsAny<ArrayOf<ByteString>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(emptyPage);
|
||||
return mock;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public sealed class OpcUaClientBrowseSessionPaginationTests
|
||||
|
||||
mock.Verify(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(), It.Is<bool>(r => r),
|
||||
It.IsAny<ByteStringCollection>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
It.IsAny<ArrayOf<ByteString>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>Positive control: an uncancelled pagination never issues a release call.</summary>
|
||||
@@ -104,9 +104,9 @@ public sealed class OpcUaClientBrowseSessionPaginationTests
|
||||
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);
|
||||
It.IsAny<ArrayOf<ByteString>>(), 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);
|
||||
It.IsAny<ArrayOf<ByteString>>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
// A loose mock answers the attribute read with no response, so nothing is known about the node.
|
||||
var mockSession = new Mock<ISession>(MockBehavior.Loose);
|
||||
mockSession.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
mockSession.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
|
||||
mockSession.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(BrowseTelemetry.Context));
|
||||
var nsMap = NamespaceMap.FromTable(new NamespaceTable());
|
||||
var sut = new OpcUaClientBrowseSession(mockSession.Object, nsMap, ObjectIds.ObjectsFolder);
|
||||
|
||||
|
||||
+28
-8
@@ -21,7 +21,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
private static Mock<ISession> NewSession()
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
return mock;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
{
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8");
|
||||
var session = NewSession();
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed));
|
||||
drv.SetSessionForTest(session.Object);
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
{
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-o8-ok");
|
||||
var session = NewSession();
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CallResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
@@ -61,10 +61,30 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
|
||||
await drv.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken);
|
||||
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()),
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Acknowledge_encodes_EventId_as_scalar_ByteString_and_never_retries_a_transport_failure()
|
||||
{
|
||||
using var driver = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-ack-types");
|
||||
var session = NewSession();
|
||||
ArrayOf<CallMethodRequest> sent = default;
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<RequestHeader, ArrayOf<CallMethodRequest>, CancellationToken>((_, calls, _) => sent = calls)
|
||||
.ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed));
|
||||
driver.SetSessionForTest(session.Object);
|
||||
await Should.ThrowAsync<ServiceResultException>(() => driver.AcknowledgeAsync(Ack(), TestContext.Current.CancellationToken));
|
||||
var arguments = sent[0].InputArguments;
|
||||
arguments[0].TypeInfo.BuiltInType.ShouldBe(BuiltInType.ByteString);
|
||||
arguments[0].TypeInfo.ValueRank.ShouldBe(ValueRanks.Scalar);
|
||||
arguments[0].GetByteString().IsEmpty.ShouldBeTrue();
|
||||
arguments[1].TypeInfo.BuiltInType.ShouldBe(BuiltInType.LocalizedText);
|
||||
arguments[1].GetLocalizedText().Text.ShouldBe("operator ack");
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>A session lost while the ack queued on the gate fails the ack rather than reporting it applied.</summary>
|
||||
[Fact]
|
||||
public async Task A_session_lost_while_queued_fails_the_ack()
|
||||
@@ -110,7 +130,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
|
||||
await drv.AcknowledgeAsync(Unresolvable(), TestContext.Current.CancellationToken);
|
||||
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()),
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
lock (logger.Entries)
|
||||
logger.Entries.ShouldContain(e => e.Level == Microsoft.Extensions.Logging.LogLevel.Warning && e.Message.Contains("resolved none"));
|
||||
@@ -127,7 +147,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-n1");
|
||||
var session = NewSession();
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CallResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
@@ -154,7 +174,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
await invoker.ExecuteAsync(DriverCapability.AlarmAcknowledge, "host",
|
||||
async c => await drv.AcknowledgeAsync(Ack(), c), ct);
|
||||
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()),
|
||||
session.Verify(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once, "the genuine ack must still reach the server");
|
||||
}
|
||||
|
||||
@@ -166,7 +186,7 @@ public sealed class OpcUaClientAcknowledgeFailureTests
|
||||
var stamp = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
drv.SetHealthForTest(new DriverHealth(DriverState.Healthy, stamp, null));
|
||||
var session = NewSession();
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.CallAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed));
|
||||
drv.SetSessionForTest(session.Object);
|
||||
|
||||
|
||||
@@ -76,9 +76,9 @@ public sealed class OpcUaClientAlarmTests
|
||||
|
||||
// ---- #619: v3 RawPath <-> upstream-source translation on the native-alarm path ----
|
||||
|
||||
private static VariantCollection EventFields(string sourceNode, string conditionNode) =>
|
||||
private static List<Variant> EventFields(string sourceNode, string conditionNode) =>
|
||||
[
|
||||
new Variant(new byte[] { 1 }), // EventId
|
||||
Variant.From(new byte[] { 1 }.ToByteString()), // scalar ByteString EventId
|
||||
new Variant(new NodeId("TripAlarmType", 3)), // EventType
|
||||
new Variant(NodeId.Parse(sourceNode)), // SourceNode
|
||||
new Variant(new LocalizedText("tripped")), // Message
|
||||
@@ -310,18 +310,18 @@ public sealed class OpcUaClientAlarmTests
|
||||
}
|
||||
}
|
||||
|
||||
private static VariantCollection ConditionFields(
|
||||
private static List<Variant> ConditionFields(
|
||||
bool active, bool acked, string message = "tripped", ushort severity = 700, byte[]? eventId = null,
|
||||
NodeId? branchId = null, NodeId? eventType = null)
|
||||
{
|
||||
var f = EventFields("ns=3;s=Alarms.Trip1", "ns=3;s=Alarms.Trip1.Cond");
|
||||
f[0] = new Variant(eventId ?? new byte[] { 1 });
|
||||
if (eventType is not null) f[1] = new Variant(eventType);
|
||||
f[0] = Variant.From(new ByteString((eventId ?? new byte[] { 1 }).AsMemory()));
|
||||
if (eventType is not null) f[1] = Variant.From(eventType.Value);
|
||||
f[3] = new Variant(new LocalizedText(message));
|
||||
f[4] = new Variant(severity);
|
||||
f.Add(new Variant(active));
|
||||
f.Add(new Variant(acked));
|
||||
f.Add(branchId is null ? Variant.Null : new Variant(branchId));
|
||||
f.Add(branchId is null ? Variant.Null : Variant.From(branchId.Value));
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ public sealed class OpcUaClientAlarmTests
|
||||
OpcUaClientDriver.IsRefreshRequired(EventFieldsOfType(ObjectTypeIds.RefreshEndEventType)).ShouldBeFalse();
|
||||
}
|
||||
|
||||
private static VariantCollection EventFieldsOfType(NodeId type)
|
||||
private static List<Variant> EventFieldsOfType(NodeId type)
|
||||
{
|
||||
var f = EventFields("i=2253", "i=0");
|
||||
f[1] = new Variant(type);
|
||||
|
||||
@@ -65,4 +65,49 @@ public sealed class OpcUaClientCertAuthTests
|
||||
try { File.Delete(tmpPath); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
[Fact]
|
||||
public async Task Certificate_identity_signs_repeatedly_and_requires_reopen_after_file_rotation()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"opcua-cert-rotation-{Guid.NewGuid():N}.pfx");
|
||||
try
|
||||
{
|
||||
using var key = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=OpcUaSdk2Signing", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
File.WriteAllBytes(path, original.Export(X509ContentType.Pfx, "test-password"));
|
||||
var options = new OpcUaClientDriverOptions { AuthType = OpcUaAuthType.Certificate,
|
||||
UserCertificatePath = path, UserCertificatePassword = "test-password" };
|
||||
var identity = OpcUaClientDriver.BuildCertificateIdentity(options);
|
||||
byte[] challenge = [1, 2, 3, 4];
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var signature = await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
||||
TestContext.Current.CancellationToken);
|
||||
using var publicKey = original.GetRSAPublicKey();
|
||||
publicKey.ShouldNotBeNull().VerifyData(challenge, signature.Signature.Span,
|
||||
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).ShouldBeTrue();
|
||||
}
|
||||
using var cancelled = new CancellationTokenSource();
|
||||
cancelled.Cancel();
|
||||
await Should.ThrowAsync<OperationCanceledException>(async () =>
|
||||
await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256, cancelled.Token));
|
||||
|
||||
using var rotatedKey = RSA.Create(2048);
|
||||
var rotatedRequest = new CertificateRequest("CN=OpcUaSdk2Signing", rotatedKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
using var rotated = rotatedRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
|
||||
File.WriteAllBytes(path, rotated.Export(X509ContentType.Pfx, "test-password"));
|
||||
var error = await Should.ThrowAsync<Opc.Ua.ServiceResultException>(async () =>
|
||||
await identity.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
||||
TestContext.Current.CancellationToken));
|
||||
error.StatusCode.ShouldBe(Opc.Ua.StatusCodes.BadIdentityTokenInvalid);
|
||||
var fresh = OpcUaClientDriver.BuildCertificateIdentity(options);
|
||||
var freshSignature = await fresh.TokenHandler.SignAsync(challenge, Opc.Ua.SecurityPolicies.Basic256Sha256,
|
||||
TestContext.Current.CancellationToken);
|
||||
using var rotatedPublicKey = rotated.GetRSAPublicKey();
|
||||
rotatedPublicKey.ShouldNotBeNull().VerifyData(challenge, freshSignature.Signature.Span,
|
||||
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).ShouldBeTrue();
|
||||
}
|
||||
finally { File.Delete(path); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-22
@@ -59,7 +59,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
private static Mock<ISession> NewSession()
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
return mock;
|
||||
}
|
||||
@@ -77,18 +77,18 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
return (drv, session, clock, log);
|
||||
}
|
||||
|
||||
private static DataValue Good(object value, DateTime ts) => new(new Variant(value), StatusCodes.Good, ts, ts);
|
||||
private static DataValue Good(object value, DateTime ts) => new(VariantHelper.CastFrom(value), StatusCodes.Good, ts, ts);
|
||||
|
||||
/// <summary>Every read returns <paramref name="values"/> (one per requested node, in order) at <paramref name="at"/>.</summary>
|
||||
private static void SetupRead(Mock<ISession> session, Func<DateTime> at, params object[] values) =>
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ArrayOf<ReadValueId> nodes, CancellationToken _) =>
|
||||
new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select((_, i) => Good(values[i], at()))],
|
||||
Results = [.. nodes.ToList().Select((_, i) => Good(values[i], at()))],
|
||||
DiagnosticInfos = [],
|
||||
});
|
||||
|
||||
@@ -96,18 +96,18 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
|
||||
public static TheoryData<uint> DefinitiveStatuses =>
|
||||
[
|
||||
StatusCodes.BadSessionIdInvalid,
|
||||
StatusCodes.BadSessionClosed,
|
||||
StatusCodes.BadSessionNotActivated,
|
||||
StatusCodes.BadServerHalted,
|
||||
StatusCodes.BadSessionIdInvalid.Code,
|
||||
StatusCodes.BadSessionClosed.Code,
|
||||
StatusCodes.BadSessionNotActivated.Code,
|
||||
StatusCodes.BadServerHalted.Code,
|
||||
];
|
||||
|
||||
public static TheoryData<uint> AmbiguousStatuses =>
|
||||
[
|
||||
StatusCodes.BadNoCommunication,
|
||||
StatusCodes.BadNotConnected,
|
||||
StatusCodes.BadRequestTimeout,
|
||||
StatusCodes.BadTimeout,
|
||||
StatusCodes.BadNoCommunication.Code,
|
||||
StatusCodes.BadNotConnected.Code,
|
||||
StatusCodes.BadRequestTimeout.Code,
|
||||
StatusCodes.BadTimeout.Code,
|
||||
];
|
||||
|
||||
[Theory]
|
||||
@@ -127,7 +127,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadSessionIdInvalid);
|
||||
|
||||
raised.ShouldHaveSingleItem("once per session, however many definitive keep-alives follow")
|
||||
.Reason.ShouldContain(StatusCodes.GetBrowseName(status));
|
||||
.Reason.ShouldContain(new StatusCode(status).SymbolicId!);
|
||||
order.ShouldBe([$"bad:{BadNoCommunication:X8}", "lost"], "subscribers see the Bad before the host tears down");
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
var reads = 0;
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() =>
|
||||
{
|
||||
var trailing = Interlocked.Increment(ref reads) > 1;
|
||||
@@ -447,7 +447,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
var reads = 0;
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async () =>
|
||||
{
|
||||
if (Interlocked.Increment(ref reads) > 1) await gate.Task; // hold the trailing read
|
||||
@@ -495,7 +495,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
|
||||
session.Verify(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()), Times.Once,
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()), Times.Once,
|
||||
"only the recovery read — the trailing read must not run against a session that is no longer current");
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
var reads = 0;
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() => new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
@@ -559,7 +559,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
var handle = drv.AddSubscriptionForTest([SpeedRef]);
|
||||
var t0 = clock.Now.UtcDateTime;
|
||||
DataValue NoSource(object v, DateTime server) =>
|
||||
new(new Variant(v), StatusCodes.Good, DateTime.MinValue, server);
|
||||
new(VariantHelper.CastFrom(v), StatusCodes.Good, DateTime.MinValue, server);
|
||||
drv.DeliverNotificationForTest(handle, SpeedRef, NoSource(10f, t0));
|
||||
var reads = SetupRecoveryThenTrailing(session, NoSource(10f, t0.AddSeconds(1)), NoSource(40f, t0.AddSeconds(9)));
|
||||
|
||||
@@ -587,7 +587,7 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
var reads = 0;
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() => new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
@@ -677,12 +677,12 @@ public sealed class OpcUaClientConnectionLossSignalTests
|
||||
drv.PostRecoveryReconcileDelay = () => TimeSpan.Zero;
|
||||
var handle = drv.AddSubscriptionForTest([SpeedRef]);
|
||||
var t0 = clock.Now.UtcDateTime;
|
||||
DataValue NoSource(object v, DateTime server) => new(new Variant(v), StatusCodes.Good, DateTime.MinValue, server);
|
||||
DataValue NoSource(object v, DateTime server) => new(VariantHelper.CastFrom(v), StatusCodes.Good, DateTime.MinValue, server);
|
||||
drv.DeliverNotificationForTest(handle, SpeedRef, NoSource(10f, t0));
|
||||
var reads = 0;
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() =>
|
||||
{
|
||||
DataValue result;
|
||||
|
||||
+21
-21
@@ -37,7 +37,7 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
private static Mock<ISession> BuildCancellingSessionMock()
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
|
||||
// BrowseAsync — return one Object child plus a non-empty continuation point so
|
||||
@@ -52,20 +52,20 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
};
|
||||
var browseResult = new BrowseResult
|
||||
{
|
||||
ContinuationPoint = FakeContinuationPoint,
|
||||
References = new ReferenceDescriptionCollection { firstRef },
|
||||
ContinuationPoint = FakeContinuationPoint.ToByteString(),
|
||||
References = new List<ReferenceDescription> { firstRef },
|
||||
};
|
||||
var browseResp = new BrowseResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new BrowseResultCollection { browseResult },
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
Results = new List<BrowseResult> { browseResult },
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
};
|
||||
mock.Setup(s => s.BrowseAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.IsAny<ViewDescription>(),
|
||||
It.IsAny<uint>(),
|
||||
It.IsAny<BrowseDescriptionCollection>(),
|
||||
It.IsAny<ArrayOf<BrowseDescription>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(browseResp);
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
mock.Setup(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.Is<bool>(release => !release),
|
||||
It.IsAny<ByteStringCollection>(),
|
||||
It.IsAny<ArrayOf<ByteString>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new OperationCanceledException("ct cancelled during BrowseNext"));
|
||||
|
||||
@@ -82,19 +82,19 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
// there is no separate BrowseNextResult type in the OPC UA SDK.
|
||||
var emptyReleaseResult = new BrowseResult
|
||||
{
|
||||
ContinuationPoint = Array.Empty<byte>(),
|
||||
References = new ReferenceDescriptionCollection(),
|
||||
ContinuationPoint = ByteString.Empty,
|
||||
References = new List<ReferenceDescription>(),
|
||||
};
|
||||
var releaseResp = new BrowseNextResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new BrowseResultCollection { emptyReleaseResult },
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
Results = new List<BrowseResult> { emptyReleaseResult },
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
};
|
||||
mock.Setup(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.Is<bool>(release => release),
|
||||
It.IsAny<ByteStringCollection>(),
|
||||
It.IsAny<ArrayOf<ByteString>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(releaseResp);
|
||||
|
||||
@@ -127,7 +127,7 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
sessionMock.Verify(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.Is<bool>(release => release),
|
||||
It.IsAny<ByteStringCollection>(),
|
||||
It.IsAny<ArrayOf<ByteString>>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once,
|
||||
"DiscoverAsync must release the server-side continuation point on cancellation " +
|
||||
@@ -148,7 +148,7 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
using var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions(), "opcua-cp-norelease");
|
||||
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
|
||||
// BrowseAsync — return a result with a continuation point so the loop is entered.
|
||||
@@ -161,27 +161,27 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
};
|
||||
var browseResult = new BrowseResult
|
||||
{
|
||||
ContinuationPoint = FakeContinuationPoint,
|
||||
References = new ReferenceDescriptionCollection { firstRef },
|
||||
ContinuationPoint = FakeContinuationPoint.ToByteString(),
|
||||
References = new List<ReferenceDescription> { firstRef },
|
||||
};
|
||||
mock.Setup(s => s.BrowseAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.IsAny<ViewDescription>(),
|
||||
It.IsAny<uint>(),
|
||||
It.IsAny<BrowseDescriptionCollection>(),
|
||||
It.IsAny<ArrayOf<BrowseDescription>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new BrowseResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new BrowseResultCollection { browseResult },
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
Results = new List<BrowseResult> { browseResult },
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
});
|
||||
|
||||
// BrowseNextAsync — throw a non-cancellation transport error.
|
||||
mock.Setup(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.Is<bool>(release => !release),
|
||||
It.IsAny<ByteStringCollection>(),
|
||||
It.IsAny<ArrayOf<ByteString>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ServiceResultException(StatusCodes.BadCommunicationError));
|
||||
|
||||
@@ -195,7 +195,7 @@ public sealed class OpcUaClientContinuationPointReleaseTests
|
||||
mock.Verify(s => s.BrowseNextAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.Is<bool>(release => release),
|
||||
It.IsAny<ByteStringCollection>(),
|
||||
It.IsAny<ArrayOf<ByteString>>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Never,
|
||||
"A transient transport failure must not trigger a release BrowseNext call");
|
||||
|
||||
+9
-9
@@ -28,16 +28,16 @@ public sealed class OpcUaClientGateSplitTests
|
||||
{
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, "opcua-o6");
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ArrayOf<ReadValueId> nodes, CancellationToken _) =>
|
||||
new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select(_ => new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
Results = [.. nodes.ToList().Select(_ => new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
DiagnosticInfos = [],
|
||||
});
|
||||
drv.SetSessionForTest(session.Object);
|
||||
@@ -60,15 +60,15 @@ public sealed class OpcUaClientGateSplitTests
|
||||
using var _ = drv;
|
||||
var writeOnWire = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var releaseWrite = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, WriteValueCollection nodes, CancellationToken _) =>
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, ArrayOf<WriteValue> nodes, CancellationToken _) =>
|
||||
{
|
||||
writeOnWire.TrySetResult();
|
||||
await releaseWrite.Task;
|
||||
return new WriteResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select(_ => (StatusCode)StatusCodes.Good)],
|
||||
Results = [.. nodes.ToList().Select(_ => (StatusCode)StatusCodes.Good)],
|
||||
DiagnosticInfos = [],
|
||||
};
|
||||
});
|
||||
@@ -111,8 +111,8 @@ public sealed class OpcUaClientGateSplitTests
|
||||
var releaseBrowse = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
session.Setup(s => s.BrowseAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<ViewDescription>(), It.IsAny<uint>(),
|
||||
It.IsAny<BrowseDescriptionCollection>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, ViewDescription _, uint _, BrowseDescriptionCollection _, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<BrowseDescription>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, ViewDescription _, uint _, ArrayOf<BrowseDescription> _, CancellationToken _) =>
|
||||
{
|
||||
browseOnWire.TrySetResult();
|
||||
await releaseBrowse.Task;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public sealed class OpcUaClientIdlePublishErrorTests
|
||||
{
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, instance);
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
drv.SetSessionForTest(session.Object);
|
||||
drv.SetNamespaceMapForTest(NamespaceMap.FromTable(new NamespaceTable(), Tags));
|
||||
|
||||
+25
-5
@@ -29,15 +29,15 @@ public sealed class OpcUaClientLifecycleGenerationTests
|
||||
private static Mock<ISession> NewSession()
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
return mock;
|
||||
}
|
||||
|
||||
private static ReadResponse GoodRead(ReadValueIdCollection nodes) => new()
|
||||
private static ReadResponse GoodRead(ArrayOf<ReadValueId> nodes) => new()
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select(_ => new DataValue(new Variant(1), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
Results = [.. nodes.ToList().Select(_ => new DataValue(new Variant(1), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
DiagnosticInfos = [],
|
||||
};
|
||||
|
||||
@@ -118,6 +118,26 @@ public sealed class OpcUaClientLifecycleGenerationTests
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Unknown, "the abandoned init must not overwrite the newer health");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancellation_at_session_ownership_transfer_discards_session_without_failover_or_install()
|
||||
{
|
||||
var session = NewSession();
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
using var driver = new OpcUaClientDriver(new OpcUaClientDriverOptions
|
||||
{ EndpointUrls = ["opc.tcp://first:4840", "opc.tcp://second:4840"] }, "sdk2-cancel-open");
|
||||
var opens = 0;
|
||||
driver.OpenSessionForTest = (_, _) =>
|
||||
{
|
||||
opens++;
|
||||
cancellation.Cancel();
|
||||
return Task.FromResult(session.Object);
|
||||
};
|
||||
await Should.ThrowAsync<OperationCanceledException>(() => driver.InitializeAsync("{}", cancellation.Token));
|
||||
opens.ShouldBe(1);
|
||||
driver.Session.ShouldBeNull();
|
||||
session.Verify(s => s.Dispose(), Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>A read whose response arrives after a shutdown does not write Healthy over the shutdown.</summary>
|
||||
[Fact]
|
||||
public async Task A_read_that_outlives_a_shutdown_does_not_overwrite_the_shutdown_health()
|
||||
@@ -160,8 +180,8 @@ public sealed class OpcUaClientLifecycleGenerationTests
|
||||
var respond = new System.Collections.Concurrent.ConcurrentQueue<TaskCompletionSource>();
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, double _, TimestampsToReturn _, ArrayOf<ReadValueId> nodes, CancellationToken _) =>
|
||||
{
|
||||
var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
respond.Enqueue(gate);
|
||||
|
||||
@@ -74,6 +74,35 @@ public sealed class OpcUaClientNamespaceTests
|
||||
NamespaceMap.TryResolve(null!, "ns=1;s=X", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
public static TheoryData<NodeId> IdentifierKinds =>
|
||||
[
|
||||
new NodeId(123u, 1),
|
||||
new NodeId("literal;i=123", 1),
|
||||
new NodeId(Guid.Parse("db8d57c4-316f-4517-819f-194c3a6db955"), 1),
|
||||
new NodeId(new byte[] { 0, 1, 127, 255 }.ToByteString(), 1),
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IdentifierKinds))]
|
||||
public void Stable_reference_rebinds_namespace_without_changing_identifier_type(NodeId original)
|
||||
{
|
||||
var stable = TestMap("http://opcfoundation.org/UA/", "urn:remote:server").ToStableReference(original);
|
||||
var context = ServiceMessageContext.Create(SdkTelemetry.Context);
|
||||
context.NamespaceUris.Append("urn:other");
|
||||
context.NamespaceUris.Append("urn:remote:server");
|
||||
var session = new Moq.Mock<Opc.Ua.Client.ISession>();
|
||||
session.SetupGet(s => s.MessageContext).Returns(context);
|
||||
NamespaceMap.TryResolve(session.Object, stable, out var rebound).ShouldBeTrue();
|
||||
rebound.NamespaceIndex.ShouldBe((ushort)2);
|
||||
rebound.IdType.ShouldBe(original.IdType);
|
||||
rebound.IdentifierAsString.ShouldBe(original.IdentifierAsString);
|
||||
if (original.IdType == IdType.Opaque)
|
||||
{
|
||||
rebound.TryGetValue(out ByteString identifier).ShouldBeTrue();
|
||||
identifier.ToArray().ShouldBe(new byte[] { 0, 1, 127, 255 });
|
||||
}
|
||||
}
|
||||
|
||||
// ---- v3: RawPath → upstream node id resolution table ----
|
||||
|
||||
/// <summary>Verifies a RawPath resolves to the upstream node id authored in its TagConfig.nodeId.</summary>
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
using Moq;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientSdkValueBoundaryTests
|
||||
{
|
||||
private const string Reference = "Plant/Upstream/Value";
|
||||
private static readonly RawTagEntry[] Tags = [new(Reference, """{"nodeId":"i=2258"}""", false)];
|
||||
|
||||
private static (OpcUaClientDriver Driver, Mock<ISession> Session) Connected()
|
||||
{
|
||||
var session = new Mock<ISession>();
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
var driver = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, "sdk2-values");
|
||||
driver.SetSessionForTest(session.Object);
|
||||
driver.SetNamespaceMapForTest(NamespaceMap.FromTable(new NamespaceTable(), Tags));
|
||||
return (driver, session);
|
||||
}
|
||||
|
||||
public static TheoryData<Variant, object?> Payloads =>
|
||||
[
|
||||
new(Variant.From((sbyte)-3), (sbyte)-3),
|
||||
new(Variant.From((byte)250), (byte)250),
|
||||
new(Variant.From((short)-30), (short)-30),
|
||||
new(Variant.From((ushort)60000), (ushort)60000),
|
||||
new(Variant.From(-100), -100),
|
||||
new(Variant.From(uint.MaxValue), uint.MaxValue),
|
||||
new(Variant.From(long.MinValue), long.MinValue),
|
||||
new(Variant.From(ulong.MaxValue), ulong.MaxValue),
|
||||
new(Variant.From(1.5f), 1.5f),
|
||||
new(Variant.From(1.5d), 1.5d),
|
||||
new(Variant.From(true), true),
|
||||
new(Variant.From("text"), "text"),
|
||||
new(Variant.Null, null),
|
||||
new(Variant.From(new DateTimeUtc(new DateTime(2026, 11, 10, 1, 2, 3, DateTimeKind.Utc))), new DateTime(2026, 11, 10, 1, 2, 3, DateTimeKind.Utc)),
|
||||
new(Variant.From(new Uuid(Guid.Parse("db8d57c4-316f-4517-819f-194c3a6db955"))), Guid.Parse("db8d57c4-316f-4517-819f-194c3a6db955")),
|
||||
new(Variant.From(new byte[] { 1, 2 }.ToByteString()), new byte[] { 1, 2 }),
|
||||
new(Variant.From(default(ByteString)), null),
|
||||
new(Variant.From(ByteString.Empty), Array.Empty<byte>()),
|
||||
new(Variant.From((ArrayOf<bool>)[true, false]), new[] { true, false }),
|
||||
new(Variant.From((ArrayOf<sbyte>)[-1, 2]), new sbyte[] { -1, 2 }),
|
||||
new(Variant.From((ArrayOf<byte>)[1, 250]), new byte[] { 1, 250 }),
|
||||
new(Variant.From((ArrayOf<short>)[1, 2]), new short[] { 1, 2 }),
|
||||
new(Variant.From((ArrayOf<ushort>)[1, 60000]), new ushort[] { 1, 60000 }),
|
||||
new(Variant.From((ArrayOf<int>)[-1, 2]), new int[] { -1, 2 }),
|
||||
new(Variant.From((ArrayOf<long>)[long.MinValue, long.MaxValue]), new long[] { long.MinValue, long.MaxValue }),
|
||||
new(Variant.From((ArrayOf<ulong>)[0, ulong.MaxValue]), new ulong[] { 0, ulong.MaxValue }),
|
||||
new(Variant.From((ArrayOf<double>)[1, 2]), new double[] { 1, 2 }),
|
||||
new(Variant.From((ArrayOf<uint>)[1, 2]), new uint[] { 1, 2 }),
|
||||
new(Variant.From((ArrayOf<float>)[1, 2]), new float[] { 1, 2 }),
|
||||
new(Variant.From((ArrayOf<string>)["a", "b"]), new[] { "a", "b" }),
|
||||
new(Variant.From((ArrayOf<DateTimeUtc>)[new DateTimeUtc(new DateTime(2026, 11, 10, 1, 2, 3, DateTimeKind.Utc))]), new[] { new DateTime(2026, 11, 10, 1, 2, 3, DateTimeKind.Utc) }),
|
||||
new(Variant.From((ArrayOf<Uuid>)[new Uuid(Guid.Empty)]), new[] { Guid.Empty }),
|
||||
new(Variant.From((ArrayOf<ByteString>)[new byte[] { 3 }.ToByteString(), ByteString.Empty]), new byte[][] { [3], [] }),
|
||||
new(Variant.From(default(ArrayOf<short>)), null),
|
||||
new(Variant.From((ArrayOf<short>)[]), Array.Empty<short>()),
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Payloads))]
|
||||
public async Task Read_and_notification_preserve_CLR_type_status_and_timestamp_rules(Variant upstream, object? expected)
|
||||
{
|
||||
var (driver, session) = Connected();
|
||||
using var _ = driver;
|
||||
session.Setup(s => s.ReadAsync(It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>())).ReturnsAsync(new ReadResponse
|
||||
{ Results = [new DataValue(upstream, StatusCodes.UncertainLastUsableValue, DateTimeUtc.MinValue, DateTimeUtc.MinValue)] });
|
||||
var before = DateTime.UtcNow;
|
||||
var read = (await driver.ReadAsync([Reference], TestContext.Current.CancellationToken)).Single();
|
||||
AssertPayload(read, expected, before);
|
||||
DataValueSnapshot? notified = null;
|
||||
driver.OnDataChange += (_, e) => notified = e.Snapshot;
|
||||
var handle = driver.AddSubscriptionForTest([Reference]);
|
||||
driver.DeliverNotificationForTest(handle, Reference,
|
||||
new DataValue(upstream, StatusCodes.UncertainLastUsableValue, DateTimeUtc.MinValue, DateTimeUtc.MinValue));
|
||||
AssertPayload(notified.ShouldNotBeNull(), expected, before);
|
||||
}
|
||||
|
||||
private static void AssertPayload(DataValueSnapshot snapshot, object? expected, DateTime before)
|
||||
{
|
||||
snapshot.Value.ShouldBe(expected);
|
||||
snapshot.Value?.GetType().ShouldBe(expected?.GetType());
|
||||
snapshot.StatusCode.ShouldBe(StatusCodes.UncertainLastUsableValue.Code);
|
||||
snapshot.SourceTimestampUtc.ShouldBeNull();
|
||||
snapshot.ServerTimestampUtc.ShouldBeGreaterThanOrEqualTo(before);
|
||||
snapshot.ServerTimestampUtc.Kind.ShouldBe(DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public async Task Read_and_notification_detach_upstream_buffers(int shape)
|
||||
{
|
||||
var (driver, session) = Connected();
|
||||
using var _ = driver;
|
||||
byte[] bytes = [1, 2];
|
||||
short[] numbers = [3, 4];
|
||||
// ReadOnlyMemory constructors deliberately alias the backing SDK buffer. Copying the
|
||||
// test input before the adapter sees it would make the ownership assertion vacuous.
|
||||
var value = shape switch
|
||||
{
|
||||
0 => Variant.From(new ByteString(bytes.AsMemory())),
|
||||
1 => Variant.From(new ArrayOf<short>(numbers.AsMemory())),
|
||||
_ => Variant.From((ArrayOf<ByteString>)[new ByteString(bytes.AsMemory())]),
|
||||
};
|
||||
object expected = shape switch { 0 => new byte[] { 1, 2 }, 1 => new short[] { 3, 4 }, _ => new byte[][] { [1, 2] } };
|
||||
session.Setup(s => s.ReadAsync(It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>())).ReturnsAsync(new ReadResponse { Results = [new DataValue(value)] });
|
||||
var read = (await driver.ReadAsync([Reference], TestContext.Current.CancellationToken)).Single();
|
||||
DataValueSnapshot? notified = null;
|
||||
driver.OnDataChange += (_, e) => notified = e.Snapshot;
|
||||
driver.DeliverNotificationForTest(driver.AddSubscriptionForTest([Reference]), Reference, new DataValue(value));
|
||||
bytes[0] = 99;
|
||||
numbers[0] = 99;
|
||||
read.Value.ShouldBe(expected);
|
||||
notified.ShouldNotBeNull().Value.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Matrix_read_and_notification_are_refused_per_item_without_materialization()
|
||||
{
|
||||
var (driver, session) = Connected();
|
||||
using var _ = driver;
|
||||
var sourceTime = new DateTime(2026, 11, 10, 1, 2, 3, DateTimeKind.Utc);
|
||||
var serverTime = sourceTime.AddSeconds(1);
|
||||
var matrix = Variant.From(new int[,] { { 1, 2 }, { 3, 4 } }.ToMatrixOf());
|
||||
session.Setup(s => s.ReadAsync(It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>())).ReturnsAsync(new ReadResponse
|
||||
{ Results = [new DataValue(matrix, StatusCodes.Good, sourceTime, serverTime),
|
||||
new DataValue(Variant.From(42), StatusCodes.Good, sourceTime, serverTime)] });
|
||||
var results = await driver.ReadAsync([Reference, Reference], TestContext.Current.CancellationToken);
|
||||
results[0].Value.ShouldBeNull();
|
||||
results[0].StatusCode.ShouldBe(StatusCodes.BadTypeMismatch.Code);
|
||||
results[0].SourceTimestampUtc.ShouldBe(sourceTime);
|
||||
results[0].ServerTimestampUtc.ShouldBe(serverTime);
|
||||
results[1].Value.ShouldBe(42);
|
||||
results[1].StatusCode.ShouldBe(StatusCodes.Good.Code);
|
||||
DataValueSnapshot? notified = null;
|
||||
driver.OnDataChange += (_, e) => notified = e.Snapshot;
|
||||
driver.DeliverNotificationForTest(driver.AddSubscriptionForTest([Reference]), Reference,
|
||||
new DataValue(matrix, StatusCodes.UncertainLastUsableValue, sourceTime, serverTime));
|
||||
notified.ShouldNotBeNull().Value.ShouldBeNull();
|
||||
notified.StatusCode.ShouldBe(StatusCodes.BadTypeMismatch.Code);
|
||||
notified.SourceTimestampUtc.ShouldBe(sourceTime);
|
||||
notified.ServerTimestampUtc.ShouldBe(serverTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_passes_scalar_shaped_arrays_once_and_rejects_unsupported_values_before_IO()
|
||||
{
|
||||
var (driver, session) = Connected();
|
||||
using var _ = driver;
|
||||
short[] caller = [7, 8];
|
||||
ArrayOf<WriteValue> sent = default;
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<RequestHeader, ArrayOf<WriteValue>, CancellationToken>((_, writes, _) => { sent = writes; caller[0] = 99; })
|
||||
.ReturnsAsync(new WriteResponse { Results = [StatusCodes.Good] });
|
||||
var result = await driver.WriteAsync([new(Reference, caller), new(Reference, new object()), new(Reference, new int[1, 1])], TestContext.Current.CancellationToken);
|
||||
result.Select(r => r.StatusCode).ShouldBe([StatusCodes.Good.Code, StatusCodes.BadTypeMismatch.Code, StatusCodes.BadTypeMismatch.Code]);
|
||||
sent.Count.ShouldBe(1);
|
||||
sent[0].Value.WrappedValue.TypeInfo.ValueRank.ShouldBe(ValueRanks.OneDimension);
|
||||
UpstreamValueMapping.ToClr(sent[0].Value.WrappedValue).ShouldBe(new short[] { 7, 8 });
|
||||
session.Verify(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -41,7 +41,7 @@ public sealed class OpcUaClientSessionLossTests
|
||||
{
|
||||
var mock = new Mock<ISession>(MockBehavior.Loose);
|
||||
mock.SetupGet(s => s.MessageContext)
|
||||
.Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
.Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
return mock;
|
||||
}
|
||||
@@ -60,17 +60,17 @@ public sealed class OpcUaClientSessionLossTests
|
||||
}
|
||||
|
||||
private static DataValue Good(object value, DateTime ts) =>
|
||||
new(new Variant(value), StatusCodes.Good, ts, ts);
|
||||
new(VariantHelper.CastFrom(value), StatusCodes.Good, ts, ts);
|
||||
|
||||
private static void SetupRead(Mock<ISession> session, params object[] values) =>
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ReadValueIdCollection nodes, CancellationToken _) =>
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((RequestHeader _, double _, TimestampsToReturn _, ArrayOf<ReadValueId> nodes, CancellationToken _) =>
|
||||
new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [.. nodes.Select((_, i) => new DataValue(new Variant(values[i]), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
Results = [.. nodes.ToList().Select((_, i) => new DataValue(VariantHelper.CastFrom(values[i]), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow))],
|
||||
DiagnosticInfos = [],
|
||||
});
|
||||
|
||||
@@ -500,11 +500,11 @@ public sealed class OpcUaClientSessionLossTests
|
||||
private static void SetupReadAt(Mock<ISession> session, object value, DateTime sourceTime) =>
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = [new DataValue(new Variant(value), StatusCodes.Good, sourceTime, sourceTime)],
|
||||
Results = [new DataValue(VariantHelper.CastFrom(value), StatusCodes.Good, sourceTime, sourceTime)],
|
||||
DiagnosticInfos = [],
|
||||
});
|
||||
|
||||
@@ -702,7 +702,7 @@ public sealed class OpcUaClientSessionLossTests
|
||||
var readAt = t0.AddSeconds(10);
|
||||
session.Setup(s => s.ReadAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
|
||||
It.IsAny<ReadValueIdCollection>(), It.IsAny<CancellationToken>()))
|
||||
It.IsAny<ArrayOf<ReadValueId>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ReadResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ public sealed class OpcUaClientStaleNoSubscriptionTests
|
||||
{
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, "opcua-763");
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
var clock = new ManualClock(new DateTimeOffset(2026, 11, 9, 12, 0, 0, TimeSpan.Zero));
|
||||
drv.Clock = clock;
|
||||
@@ -209,7 +209,7 @@ public sealed class OpcUaClientSlowDeleteTests
|
||||
RawTagEntry[] tags = [new(SpeedRef, JsonSerializer.Serialize(new { nodeId = "ns=0;s=Speed" }), WriteIdempotent: true)];
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = tags }, "opcua-764");
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
var clock = new ManualClock(new DateTimeOffset(2026, 11, 10, 12, 0, 0, TimeSpan.Zero));
|
||||
drv.Clock = clock;
|
||||
|
||||
+6
-6
@@ -46,20 +46,20 @@ public sealed class OpcUaClientStaleSessionRaceTests
|
||||
// MessageContext. This getter is read in the driver's synchronous prologue (before the
|
||||
// gate), so signalling here proves the pre-gate capture path has run.
|
||||
mock.SetupGet(s => s.MessageContext)
|
||||
.Returns(new ServiceMessageContext(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context))
|
||||
.Returns(ServiceMessageContext.Create(ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.SdkTelemetry.Context))
|
||||
.Callback(() => prologueReached?.TrySetResult());
|
||||
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
// Acknowledge issues its wire call via CallAsync; stub it so AcknowledgeAsync can run to
|
||||
// completion against whichever session it ends up binding to.
|
||||
mock.Setup(s => s.CallAsync(
|
||||
It.IsAny<RequestHeader>(),
|
||||
It.IsAny<CallMethodRequestCollection>(),
|
||||
It.IsAny<ArrayOf<CallMethodRequest>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CallResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
Results = new CallMethodResultCollection(),
|
||||
DiagnosticInfos = new DiagnosticInfoCollection(),
|
||||
Results = new List<CallMethodResult>(),
|
||||
DiagnosticInfos = new List<DiagnosticInfo>(),
|
||||
});
|
||||
return mock;
|
||||
}
|
||||
@@ -67,10 +67,10 @@ public sealed class OpcUaClientStaleSessionRaceTests
|
||||
private static void VerifyAckWireHitNewSessionNotOld(Mock<ISession> newSession, Mock<ISession> oldSession)
|
||||
{
|
||||
newSession.Verify(s => s.CallAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()),
|
||||
It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
oldSession.Verify(s => s.CallAsync(
|
||||
It.IsAny<RequestHeader>(), It.IsAny<CallMethodRequestCollection>(), It.IsAny<CancellationToken>()),
|
||||
It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<CallMethodRequest>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ public sealed class OpcUaClientSubscribeCleanupTests
|
||||
{
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, "opcua-o1");
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
drv.SetSessionForTest(session.Object);
|
||||
drv.SetNamespaceMapForTest(NamespaceMap.FromTable(new NamespaceTable(), Tags));
|
||||
|
||||
+8
-8
@@ -28,7 +28,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
{
|
||||
var drv = new OpcUaClientDriver(new OpcUaClientDriverOptions { RawTags = Tags }, "opcua-write-health");
|
||||
var session = new Mock<ISession>(MockBehavior.Loose);
|
||||
session.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.MessageContext).Returns(ServiceMessageContext.Create(SdkTelemetry.Context));
|
||||
session.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
drv.SetSessionForTest(session.Object);
|
||||
drv.SetNamespaceMapForTest(NamespaceMap.FromTable(new NamespaceTable(), Tags));
|
||||
@@ -44,7 +44,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
{
|
||||
var (drv, session) = NewConnectedDriver(new DriverHealth(DriverState.Degraded, Stale, "read failed"));
|
||||
using var _ = drv;
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new WriteResponse
|
||||
{
|
||||
ResponseHeader = new ResponseHeader(),
|
||||
@@ -54,7 +54,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
|
||||
var results = await drv.WriteAsync(Write(), TestContext.Current.CancellationToken);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(StatusCodes.BadNotWritable, "the upstream refusal still passes through verbatim");
|
||||
results.Single().StatusCode.ShouldBe(StatusCodes.BadNotWritable.Code, "the upstream refusal still passes through verbatim");
|
||||
var h = drv.GetHealth();
|
||||
h.State.ShouldBe(DriverState.Healthy);
|
||||
h.LastSuccessfulRead.ShouldNotBeNull().ShouldBeGreaterThan(Stale);
|
||||
@@ -67,7 +67,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
{
|
||||
var (drv, session) = NewConnectedDriver(new DriverHealth(DriverState.Healthy, Stale, null));
|
||||
using var _ = drv;
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ServiceResultException(StatusCodes.BadConnectionClosed));
|
||||
|
||||
var results = await drv.WriteAsync(Write(), TestContext.Current.CancellationToken);
|
||||
@@ -89,8 +89,8 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
var (drv, session) = NewConnectedDriver(healthy);
|
||||
using var _ = drv;
|
||||
using var cts = new CancellationTokenSource();
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, WriteValueCollection _, CancellationToken ct) =>
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(async (RequestHeader _, ArrayOf<WriteValue> _, CancellationToken ct) =>
|
||||
{
|
||||
cts.Cancel();
|
||||
await Task.Delay(Timeout.Infinite, ct);
|
||||
@@ -128,7 +128,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
{
|
||||
var (drv, session) = NewConnectedDriver(new DriverHealth(DriverState.Healthy, Stale, null));
|
||||
using var _ = drv;
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new OperationCanceledException("SDK request timeout"));
|
||||
|
||||
var results = await drv.WriteAsync(Write(), TestContext.Current.CancellationToken);
|
||||
@@ -149,7 +149,7 @@ public sealed class OpcUaClientWriteHealthTests
|
||||
using var _ = drv;
|
||||
drv.RaiseKeepAliveForTest(StatusCodes.BadNoCommunication);
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Degraded, "positive control: the outage is published");
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<WriteValueCollection>(), It.IsAny<CancellationToken>()))
|
||||
session.Setup(s => s.WriteAsync(It.IsAny<RequestHeader>(), It.IsAny<ArrayOf<WriteValue>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new WriteResponse { ResponseHeader = new ResponseHeader(), Results = [StatusCodes.Good], DiagnosticInfos = [] });
|
||||
|
||||
await drv.WriteAsync(Write(), TestContext.Current.CancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user