mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
485 lines
21 KiB
C#
485 lines
21 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Client.Tests
|
|
{
|
|
[TestFixture]
|
|
[Category("Client")]
|
|
[Category("SessionClientExtensions")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
public sealed class SessionClientExtensionsTests
|
|
{
|
|
[Test]
|
|
public async Task ReadValuesAsyncWithMatchingTypesReturnsValuesAndErrorsAsync()
|
|
{
|
|
using ISession session = CreateSession(
|
|
readResponseFactory: _ => new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new DataValue(new Variant(13u)),
|
|
new DataValue(new Variant("hello"))
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
(ArrayOf<Variant> values, ArrayOf<ServiceResult> errors) = await session.ReadValuesAsync(
|
|
[new NodeId(1, 0), new NodeId(2, 0)],
|
|
[TypeInfo.Scalars.UInt32, TypeInfo.Scalars.String]).ConfigureAwait(false);
|
|
|
|
Assert.That(values.Count, Is.EqualTo(2));
|
|
Assert.That(values[0].TryGetValue(out uint first), Is.True);
|
|
Assert.That(first, Is.EqualTo(13u));
|
|
Assert.That(values[1].TryGetValue(out string? second), Is.True);
|
|
Assert.That(second, Is.EqualTo("hello"));
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
Assert.That(errors[1].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReadValuesAsyncWithMismatchedTypesReturnsTypeMismatchErrorsAsync()
|
|
{
|
|
using ISession session = CreateSession(
|
|
readResponseFactory: _ => new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new DataValue(new Variant(13u)),
|
|
new DataValue(new Variant("hello"))
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
(ArrayOf<Variant> values, ArrayOf<ServiceResult> errors) = await session.ReadValuesAsync(
|
|
[new NodeId(1, 0), new NodeId(2, 0)],
|
|
[TypeInfo.Scalars.String, TypeInfo.Scalars.UInt32]).ConfigureAwait(false);
|
|
|
|
Assert.That(values.Count, Is.EqualTo(2));
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
|
|
Assert.That(errors[1].StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch));
|
|
}
|
|
|
|
[Test]
|
|
public async Task BrowseAsyncWithGoodResultReturnsContinuationAndReferencesAsync()
|
|
{
|
|
ReferenceDescription reference = new()
|
|
{
|
|
NodeId = new ExpandedNodeId(new NodeId(2, 0)),
|
|
BrowseName = QualifiedName.From("Child"),
|
|
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
|
IsForward = true
|
|
};
|
|
|
|
using ISession session = CreateSession(
|
|
browseResponseFactory: _ => new BrowseResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new BrowseResult
|
|
{
|
|
StatusCode = StatusCodes.Good,
|
|
ContinuationPoint = ByteString.From([1, 2, 3]),
|
|
References = [reference]
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
(ResponseHeader responseHeader, ByteString continuationPoint, ArrayOf<ReferenceDescription> references) =
|
|
await session.BrowseAsync(
|
|
null,
|
|
null,
|
|
new NodeId(1, 0),
|
|
0,
|
|
BrowseDirection.Forward,
|
|
ReferenceTypeIds.Organizes,
|
|
false,
|
|
0).ConfigureAwait(false);
|
|
|
|
Assert.That(responseHeader, Is.Not.Null);
|
|
Assert.That(continuationPoint, Is.EqualTo(ByteString.From([1, 2, 3])));
|
|
Assert.That(references, Has.Count.EqualTo(1));
|
|
Assert.That(references[0].BrowseName, Is.EqualTo(QualifiedName.From("Child")));
|
|
}
|
|
|
|
[Test]
|
|
public void BrowseWithBadResultThrowsServiceResultException()
|
|
{
|
|
using ISession session = CreateSession(
|
|
browseResponseFactory: _ => new BrowseResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new BrowseResult
|
|
{
|
|
StatusCode = StatusCodes.BadUnexpectedError,
|
|
References = []
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(async () => await session.BrowseAsync(
|
|
null,
|
|
null,
|
|
new NodeId(1, 0),
|
|
0,
|
|
BrowseDirection.Forward,
|
|
ReferenceTypeIds.Organizes,
|
|
false,
|
|
0).ConfigureAwait(false));
|
|
}
|
|
|
|
[Test]
|
|
public async Task BrowseNextAsyncWithGoodResultReturnsRevisedContinuationAndReferencesAsync()
|
|
{
|
|
ReferenceDescription reference = new()
|
|
{
|
|
NodeId = new ExpandedNodeId(new NodeId(2, 0)),
|
|
BrowseName = QualifiedName.From("Child"),
|
|
ReferenceTypeId = ReferenceTypeIds.Organizes,
|
|
IsForward = true
|
|
};
|
|
|
|
using ISession session = CreateSession(
|
|
browseNextResponseFactory: _ => new BrowseNextResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new BrowseResult
|
|
{
|
|
StatusCode = StatusCodes.Good,
|
|
ContinuationPoint = ByteString.From([4, 5, 6]),
|
|
References = [reference]
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
(ResponseHeader responseHeader, ByteString revisedContinuationPoint, ArrayOf<ReferenceDescription> references) =
|
|
await session.BrowseNextAsync(
|
|
null,
|
|
false,
|
|
ByteString.From([1, 2, 3])).ConfigureAwait(false);
|
|
|
|
Assert.That(responseHeader, Is.Not.Null);
|
|
Assert.That(revisedContinuationPoint, Is.EqualTo(ByteString.From([4, 5, 6])));
|
|
Assert.That(references, Has.Count.EqualTo(1));
|
|
Assert.That(references[0].BrowseName, Is.EqualTo(QualifiedName.From("Child")));
|
|
}
|
|
|
|
[Test]
|
|
public void BrowseNextWithBadResultThrowsServiceResultException()
|
|
{
|
|
using ISession session = CreateSession(
|
|
browseNextResponseFactory: _ => new BrowseNextResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new BrowseResult
|
|
{
|
|
StatusCode = StatusCodes.BadUnexpectedError,
|
|
References = []
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(async () => await session.BrowseNextAsync(
|
|
null,
|
|
false,
|
|
ByteString.From([1, 2, 3])).ConfigureAwait(false));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReadBytesAsyncWithSmallChunkReturnsChunkAsync()
|
|
{
|
|
using ISession session = CreateSession(
|
|
readResponseFactory: _ => new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results = [new DataValue(new Variant(ByteString.From([1, 2, 3])))],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
ByteString result = await session.ReadBytesAsync(new NodeId(1, 0), 10).ConfigureAwait(false);
|
|
|
|
Assert.That(result.ToArray(), Is.EqualTo(new byte[] { 1, 2, 3 }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ReadBytesAsyncWithChunkedResponseConcatenatesChunksAsync()
|
|
{
|
|
using ISession session = CreateSession();
|
|
var capturedRequests = new List<ReadValueId>();
|
|
int responseIndex = 0;
|
|
var readSetup = Mock.Get(session);
|
|
readSetup
|
|
.Setup(s => s.ReadAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<double>(),
|
|
It.IsAny<TimestampsToReturn>(),
|
|
It.IsAny<ArrayOf<ReadValueId>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, double, TimestampsToReturn, ArrayOf<ReadValueId>, CancellationToken>(
|
|
(_, _, _, requests, _) =>
|
|
{
|
|
capturedRequests.Add(requests[0]);
|
|
ReadResponse response = responseIndex++ switch
|
|
{
|
|
0 => new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new DataValue(new Variant(
|
|
ByteString.From([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])))
|
|
],
|
|
DiagnosticInfos = []
|
|
},
|
|
1 => new ReadResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results = [new DataValue(new Variant(ByteString.From([11, 12, 13])))],
|
|
DiagnosticInfos = []
|
|
},
|
|
_ => throw new InvalidOperationException("Unexpected additional chunk read.")
|
|
};
|
|
return new ValueTask<ReadResponse>(response);
|
|
});
|
|
|
|
var nodeId = new NodeId(1, 0);
|
|
ByteString result = await session.ReadBytesAsync(nodeId, 10).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
result.ToArray(),
|
|
Is.EqualTo(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }));
|
|
Assert.That(capturedRequests, Has.Count.EqualTo(2));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(capturedRequests[0].NodeId, Is.EqualTo(nodeId));
|
|
Assert.That(capturedRequests[0].AttributeId, Is.EqualTo(Attributes.Value));
|
|
Assert.That(capturedRequests[0].DataEncoding, Is.EqualTo(QualifiedName.Null));
|
|
Assert.That(capturedRequests[0].IndexRange, Is.EqualTo("0:9"));
|
|
Assert.That(capturedRequests[1].NodeId, Is.EqualTo(nodeId));
|
|
Assert.That(capturedRequests[1].AttributeId, Is.EqualTo(Attributes.Value));
|
|
Assert.That(capturedRequests[1].DataEncoding, Is.EqualTo(QualifiedName.Null));
|
|
Assert.That(capturedRequests[1].IndexRange, Is.EqualTo("10:19"));
|
|
});
|
|
readSetup.Verify(s => s.ReadAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<double>(),
|
|
It.IsAny<TimestampsToReturn>(),
|
|
It.IsAny<ArrayOf<ReadValueId>>(),
|
|
It.IsAny<CancellationToken>()), Times.Exactly(2));
|
|
}
|
|
|
|
[Test]
|
|
public async Task CallAsyncWithGoodResultReturnsOutputArgumentsAsync()
|
|
{
|
|
var capturedRequests = ArrayOf<CallMethodRequest>.Empty;
|
|
using ISession session = CreateSession(
|
|
callResponseFactory: requests =>
|
|
{
|
|
capturedRequests = requests;
|
|
return new CallResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new CallMethodResult
|
|
{
|
|
StatusCode = StatusCodes.Good,
|
|
OutputArguments = [Variant.From(42u)]
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
};
|
|
});
|
|
|
|
ArrayOf<Variant> outputArguments = await session.CallAsync(
|
|
new NodeId(1, 0),
|
|
new NodeId(2, 0),
|
|
default,
|
|
new Variant("input")).ConfigureAwait(false);
|
|
|
|
Assert.That(capturedRequests.Count, Is.EqualTo(1));
|
|
Assert.That(capturedRequests[0].ObjectId, Is.EqualTo(new NodeId(1, 0)));
|
|
Assert.That(capturedRequests[0].MethodId, Is.EqualTo(new NodeId(2, 0)));
|
|
Assert.That(outputArguments.Count, Is.EqualTo(1));
|
|
Assert.That(outputArguments[0].TryGetValue(out uint value), Is.True);
|
|
Assert.That(value, Is.EqualTo(42u));
|
|
}
|
|
|
|
[Test]
|
|
public void CallWithBadResultThrowsServiceResultException()
|
|
{
|
|
using ISession session = CreateSession(
|
|
callResponseFactory: _ => new CallResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new CallMethodResult
|
|
{
|
|
StatusCode = StatusCodes.BadUnexpectedError,
|
|
OutputArguments = []
|
|
}
|
|
],
|
|
DiagnosticInfos = []
|
|
});
|
|
|
|
Assert.ThrowsAsync<ServiceResultException>(async () => await session.CallAsync(
|
|
new NodeId(1, 0),
|
|
new NodeId(2, 0),
|
|
default,
|
|
new Variant("input")).ConfigureAwait(false));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResendDataAsyncReturnsStatusPerSubscriptionAsync()
|
|
{
|
|
var capturedRequests = ArrayOf<CallMethodRequest>.Empty;
|
|
using ISession session = CreateSession(
|
|
callResponseFactory: requests =>
|
|
{
|
|
capturedRequests = requests;
|
|
return new CallResponse
|
|
{
|
|
ResponseHeader = new ResponseHeader(),
|
|
Results =
|
|
[
|
|
new CallMethodResult { StatusCode = StatusCodes.Good },
|
|
new CallMethodResult { StatusCode = StatusCodes.BadUnexpectedError }
|
|
],
|
|
DiagnosticInfos = []
|
|
};
|
|
});
|
|
|
|
ArrayOf<ServiceResult> errors = await session.ResendDataAsync([1u, 2u]).ConfigureAwait(false);
|
|
|
|
Assert.That(capturedRequests, Has.Count.EqualTo(2));
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(capturedRequests[0].ObjectId, Is.EqualTo(ObjectIds.Server));
|
|
Assert.That(capturedRequests[0].MethodId, Is.EqualTo(MethodIds.Server_ResendData));
|
|
Assert.That(capturedRequests[0].InputArguments, Has.Count.EqualTo(1));
|
|
Assert.That(capturedRequests[0].InputArguments[0].TryGetValue(out uint first), Is.True);
|
|
Assert.That(first, Is.EqualTo(1u));
|
|
Assert.That(capturedRequests[1].ObjectId, Is.EqualTo(ObjectIds.Server));
|
|
Assert.That(capturedRequests[1].MethodId, Is.EqualTo(MethodIds.Server_ResendData));
|
|
Assert.That(capturedRequests[1].InputArguments, Has.Count.EqualTo(1));
|
|
Assert.That(capturedRequests[1].InputArguments[0].TryGetValue(out uint second), Is.True);
|
|
Assert.That(second, Is.EqualTo(2u));
|
|
});
|
|
Assert.That(errors.Count, Is.EqualTo(2));
|
|
Assert.That(errors[0].StatusCode, Is.EqualTo(StatusCodes.Good));
|
|
Assert.That(errors[1].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError));
|
|
}
|
|
|
|
private static ISession CreateSession(
|
|
Func<ArrayOf<ReadValueId>, ReadResponse>? readResponseFactory = null,
|
|
Func<ArrayOf<BrowseDescription>, BrowseResponse>? browseResponseFactory = null,
|
|
Func<ArrayOf<ByteString>, BrowseNextResponse>? browseNextResponseFactory = null,
|
|
Func<ArrayOf<CallMethodRequest>, CallResponse>? callResponseFactory = null)
|
|
{
|
|
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
|
|
var messageContext = ServiceMessageContext.Create(telemetry);
|
|
messageContext.MaxByteStringLength = 1024;
|
|
|
|
var session = new Mock<ISession>(MockBehavior.Loose);
|
|
session.SetupGet(s => s.MessageContext).Returns(messageContext);
|
|
session.SetupGet(s => s.NamespaceUris).Returns(messageContext.NamespaceUris);
|
|
session.SetupGet(s => s.ServerUris).Returns(messageContext.ServerUris);
|
|
|
|
if (readResponseFactory != null)
|
|
{
|
|
session.Setup(s => s.ReadAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<double>(),
|
|
It.IsAny<TimestampsToReturn>(),
|
|
It.IsAny<ArrayOf<ReadValueId>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, double, TimestampsToReturn, ArrayOf<ReadValueId>, CancellationToken>(
|
|
(_, _, _, readValueIds, _) => new ValueTask<ReadResponse>(readResponseFactory(readValueIds)));
|
|
}
|
|
|
|
if (browseResponseFactory != null)
|
|
{
|
|
session.Setup(s => s.BrowseAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<ViewDescription>(),
|
|
It.IsAny<uint>(),
|
|
It.IsAny<ArrayOf<BrowseDescription>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, ViewDescription, uint, ArrayOf<BrowseDescription>, CancellationToken>(
|
|
(_, _, _, browseDescriptions, _) => new ValueTask<BrowseResponse>(browseResponseFactory(browseDescriptions)));
|
|
}
|
|
|
|
if (browseNextResponseFactory != null)
|
|
{
|
|
session.Setup(s => s.BrowseNextAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<ArrayOf<ByteString>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, bool, ArrayOf<ByteString>, CancellationToken>(
|
|
(_, _, continuationPoints, _) => new ValueTask<BrowseNextResponse>(browseNextResponseFactory(continuationPoints)));
|
|
}
|
|
|
|
if (callResponseFactory != null)
|
|
{
|
|
session.Setup(s => s.CallAsync(
|
|
It.IsAny<RequestHeader>(),
|
|
It.IsAny<ArrayOf<CallMethodRequest>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Returns<RequestHeader, ArrayOf<CallMethodRequest>, CancellationToken>(
|
|
(_, requests, _) => new ValueTask<CallResponse>(callResponseFactory(requests)));
|
|
}
|
|
|
|
return session.Object;
|
|
}
|
|
}
|
|
}
|