mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
PubSub server: organize PublishedDataSets under PublishSubscribe.PublishedDataSets (#4793)
# Description PubSubNodeManager looked up the parent of the PublishedDataSet and DataSetFolder objects it materializes by NodeId i=14478. That is DataSetFolderType's <DataSetFolderName> placeholder, an InstanceDeclaration of the type, not the PublishSubscribe.PublishedDataSets object (i=17371). Every PublishedDataSet therefore became a child of the type definition and the standard PublishedDataSets folder stayed empty. Use ObjectIds.PublishSubscribe_PublishedDataSets as the parent. The test harnesses now expose the real folder and the placeholder, and a new test asserts that a PublishedDataSet ends up under the folder and that the placeholder gains no children. ## Related Issues None. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
This commit is contained in:
+5
-1
@@ -1440,6 +1440,8 @@ What the server side adds:
|
||||
`PubSubKeyPushTargetFolder`.
|
||||
- One Object per `PubSubConnection`, `WriterGroup`, `ReaderGroup`,
|
||||
`DataSetWriter`, `DataSetReader`, `PublishedDataSet`, and DataSet folder.
|
||||
PublishedDataSets and DataSet folders are organized below the standard
|
||||
`PublishSubscribe.PublishedDataSets` folder (`i=17371`).
|
||||
- Per-instance `Status` / `State` and `ConfigurationVersion` Variables so a
|
||||
client can observe the same runtime state the scheduler uses.
|
||||
2. Method bindings ([§9.1.5](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/9.1.5)):
|
||||
@@ -1447,7 +1449,9 @@ What the server side adds:
|
||||
`AddReaderGroup`, `AddDataSetWriter`, `AddDataSetReader`, `Remove*`,
|
||||
`Enable`, and `Disable`, plus `AddPublishedDataItems`,
|
||||
`AddPublishedEvents`, `AddPublishedDataItemsTemplate`, `AddVariables`,
|
||||
`RemoveVariables`, `AddDataSetFolder`, and `RemoveDataSetFolder`.
|
||||
`RemoveVariables`, `AddDataSetFolder`, and `RemoveDataSetFolder`. The
|
||||
folder methods are bound on the `PublishSubscribe.PublishedDataSets`
|
||||
instance and on every DataSet folder created through `AddDataSetFolder`.
|
||||
3. `PubSubConfigurationType` File import/export: clients can open/read the
|
||||
current `PubSubConfigurationDataType` file or write a replacement file; the
|
||||
server applies it through `ReplaceConfigurationAsync` and returns per-item
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,13 +87,7 @@ namespace Opc.Ua.PubSub.Server
|
||||
private const uint RemoveSecurityGroupNodeId = 15447;
|
||||
private const uint AddPushTargetNodeId = 25441;
|
||||
private const uint RemovePushTargetNodeId = 25444;
|
||||
private const uint AddPublishedDataItemsNodeId = 14479;
|
||||
private const uint AddPublishedEventsNodeId = 14482;
|
||||
private const uint AddPublishedDataItemsTemplateNodeId = 16842;
|
||||
private const uint RemovePublishedDataSetNodeId = 14485;
|
||||
private const uint AddDataSetFolderNodeId = 16884;
|
||||
private const uint RemoveDataSetFolderNodeId = 16923;
|
||||
private static readonly NodeId s_publishedDataSetsNodeId = new(14478u);
|
||||
private static readonly NodeId s_publishedDataSetsNodeId = ObjectIds.PublishSubscribe_PublishedDataSets;
|
||||
private static readonly NodeId s_securityGroupsNodeId = new(15443u);
|
||||
private static readonly NodeId s_keyPushTargetsNodeId = new(25440u);
|
||||
|
||||
@@ -312,6 +306,118 @@ namespace Opc.Ua.PubSub.Server
|
||||
BindStandardMethod(file.ReserveIds, OnReservePubSubConfigurationIds);
|
||||
BindStandardMethod(file.CloseAndUpdate, OnCloseAndUpdatePubSubConfigurationFile);
|
||||
diagnosticsNodeManager.AddNode(file);
|
||||
|
||||
// The DataSetFolderType methods of PublishSubscribe.PublishedDataSets
|
||||
// are Optional, so the standard address space does not contain them.
|
||||
if (publishSubscribe.PublishedDataSets is { } publishedDataSets)
|
||||
{
|
||||
foreach (MethodState method in AddPublishedDataSetsMethods(SystemContext, publishedDataSets))
|
||||
{
|
||||
diagnosticsNodeManager.AddNode(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes the implemented DataSetFolderType methods on the standard
|
||||
/// <c>PublishSubscribe.PublishedDataSets</c> instance with their well-known
|
||||
/// instance NodeIds. The generated <c>Add*</c> extensions create them with the
|
||||
/// NodeIds of the DataSetFolderType declarations, which the address space
|
||||
/// already holds.
|
||||
/// </summary>
|
||||
internal static ArrayOf<MethodState> AddPublishedDataSetsMethods(
|
||||
ISystemContext context,
|
||||
DataSetFolderState publishedDataSets)
|
||||
{
|
||||
publishedDataSets.AddAddPublishedDataItems(context);
|
||||
publishedDataSets.AddAddPublishedEvents(context);
|
||||
publishedDataSets.AddAddPublishedDataItemsTemplate(context);
|
||||
publishedDataSets.AddRemovePublishedDataSet(context);
|
||||
publishedDataSets.AddAddDataSetFolder(context);
|
||||
publishedDataSets.AddRemoveDataSetFolder(context);
|
||||
|
||||
return
|
||||
[
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.AddPublishedDataItems,
|
||||
nameof(publishedDataSets.AddPublishedDataItems),
|
||||
PublishedDataSetsIds.AddPublishedDataItems,
|
||||
PublishedDataSetsIds.AddPublishedDataItemsInputArguments,
|
||||
PublishedDataSetsIds.AddPublishedDataItemsOutputArguments),
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.AddPublishedEvents,
|
||||
nameof(publishedDataSets.AddPublishedEvents),
|
||||
PublishedDataSetsIds.AddPublishedEvents,
|
||||
PublishedDataSetsIds.AddPublishedEventsInputArguments,
|
||||
PublishedDataSetsIds.AddPublishedEventsOutputArguments),
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.AddPublishedDataItemsTemplate,
|
||||
nameof(publishedDataSets.AddPublishedDataItemsTemplate),
|
||||
PublishedDataSetsIds.AddPublishedDataItemsTemplate,
|
||||
PublishedDataSetsIds.AddPublishedDataItemsTemplateInputArguments,
|
||||
PublishedDataSetsIds.AddPublishedDataItemsTemplateOutputArguments),
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.RemovePublishedDataSet,
|
||||
nameof(publishedDataSets.RemovePublishedDataSet),
|
||||
PublishedDataSetsIds.RemovePublishedDataSet,
|
||||
PublishedDataSetsIds.RemovePublishedDataSetInputArguments,
|
||||
outputArgumentsId: null),
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.AddDataSetFolder,
|
||||
nameof(publishedDataSets.AddDataSetFolder),
|
||||
PublishedDataSetsIds.AddDataSetFolder,
|
||||
PublishedDataSetsIds.AddDataSetFolderInputArguments,
|
||||
PublishedDataSetsIds.AddDataSetFolderOutputArguments),
|
||||
AssignInstanceIds(
|
||||
publishedDataSets.RemoveDataSetFolder,
|
||||
nameof(publishedDataSets.RemoveDataSetFolder),
|
||||
PublishedDataSetsIds.RemoveDataSetFolder,
|
||||
PublishedDataSetsIds.RemoveDataSetFolderInputArguments,
|
||||
outputArgumentsId: null)
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives a materialized method and its argument variables their instance
|
||||
/// NodeIds. A method without output arguments passes a null
|
||||
/// <paramref name="outputArgumentsId"/>; an argument variable the generated
|
||||
/// method does not match is a configuration error.
|
||||
/// </summary>
|
||||
internal static MethodState AssignInstanceIds(
|
||||
MethodState? method,
|
||||
string name,
|
||||
uint methodId,
|
||||
uint inputArgumentsId,
|
||||
uint? outputArgumentsId)
|
||||
{
|
||||
if (method == null)
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadConfigurationError,
|
||||
$"The generated PublishedDataSets method {name} (i={methodId}) was not created.");
|
||||
}
|
||||
if (method.InputArguments == null)
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadConfigurationError,
|
||||
$"The generated PublishedDataSets method {name} (i={methodId}) has no InputArguments.");
|
||||
}
|
||||
if (method.OutputArguments != null && outputArgumentsId == null)
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadConfigurationError,
|
||||
$"The generated PublishedDataSets method {name} (i={methodId}) has OutputArguments " +
|
||||
"that its standard declaration does not have.");
|
||||
}
|
||||
if (method.OutputArguments == null && outputArgumentsId != null)
|
||||
{
|
||||
throw new ServiceResultException(StatusCodes.BadConfigurationError,
|
||||
$"The generated PublishedDataSets method {name} (i={methodId}) has no OutputArguments.");
|
||||
}
|
||||
method.NodeId = new NodeId(methodId);
|
||||
method.InputArguments.NodeId = new NodeId(inputArgumentsId);
|
||||
if (method.OutputArguments != null && outputArgumentsId is uint outputId)
|
||||
{
|
||||
method.OutputArguments.NodeId = new NodeId(outputId);
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
private void BindMethods(IDiagnosticsNodeManager diagnosticsNodeManager)
|
||||
@@ -373,28 +479,44 @@ namespace Opc.Ua.PubSub.Server
|
||||
m_methodsBound = enable is not null || disable is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the methods of the standard <c>PublishSubscribe.PublishedDataSets</c>
|
||||
/// instance, the folder that holds the PublishedDataSets and DataSetFolders.
|
||||
/// </summary>
|
||||
private void BindPublishedDataSetFolderMethods(IDiagnosticsNodeManager diagnosticsNodeManager)
|
||||
{
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager, AddPublishedDataItemsNodeId, MethodHandlers.OnAddPublishedDataItems);
|
||||
BindStandardMethod(diagnosticsNodeManager, AddPublishedEventsNodeId, MethodHandlers.OnAddPublishedEvents);
|
||||
diagnosticsNodeManager,
|
||||
new NodeId(PublishedDataSetsIds.AddPublishedDataItems),
|
||||
MethodHandlers.OnAddPublishedDataItems);
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager,
|
||||
AddPublishedDataItemsTemplateNodeId,
|
||||
new NodeId(PublishedDataSetsIds.AddPublishedEvents),
|
||||
MethodHandlers.OnAddPublishedEvents);
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager,
|
||||
new NodeId(PublishedDataSetsIds.AddPublishedDataItemsTemplate),
|
||||
MethodHandlers.OnAddPublishedDataItemsTemplate);
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager, RemovePublishedDataSetNodeId, MethodHandlers.OnRemovePublishedDataSet);
|
||||
BindStandardMethod(diagnosticsNodeManager, AddDataSetFolderNodeId, OnAddDataSetFolder);
|
||||
BindStandardMethod(diagnosticsNodeManager, RemoveDataSetFolderNodeId, OnRemoveDataSetFolder);
|
||||
diagnosticsNodeManager,
|
||||
new NodeId(PublishedDataSetsIds.RemovePublishedDataSet),
|
||||
MethodHandlers.OnRemovePublishedDataSet);
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager,
|
||||
new NodeId(PublishedDataSetsIds.AddDataSetFolder),
|
||||
OnAddDataSetFolder);
|
||||
BindStandardMethod(
|
||||
diagnosticsNodeManager,
|
||||
new NodeId(PublishedDataSetsIds.RemoveDataSetFolder),
|
||||
OnRemoveDataSetFolder);
|
||||
}
|
||||
|
||||
private static void BindStandardMethod(
|
||||
IDiagnosticsNodeManager diagnosticsNodeManager,
|
||||
uint nodeId,
|
||||
NodeId nodeId,
|
||||
GenericMethodCalledEventHandler handler)
|
||||
{
|
||||
MethodState? method = diagnosticsNodeManager
|
||||
.FindPredefinedNode<MethodState>(new NodeId(nodeId));
|
||||
MethodState? method = diagnosticsNodeManager.FindPredefinedNode<MethodState>(nodeId);
|
||||
BindStandardMethod(method, handler);
|
||||
}
|
||||
|
||||
@@ -1907,6 +2029,31 @@ namespace Opc.Ua.PubSub.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The NodeIds of the methods of the <c>PublishSubscribe.PublishedDataSets</c>
|
||||
/// instance (i=17371) and of their argument variables, not those of the
|
||||
/// DataSetFolderType declarations.
|
||||
/// </summary>
|
||||
private static class PublishedDataSetsIds
|
||||
{
|
||||
public const uint AddPublishedDataItems = 17372;
|
||||
public const uint AddPublishedDataItemsInputArguments = 17373;
|
||||
public const uint AddPublishedDataItemsOutputArguments = 17374;
|
||||
public const uint AddPublishedEvents = 17375;
|
||||
public const uint AddPublishedEventsInputArguments = 17376;
|
||||
public const uint AddPublishedEventsOutputArguments = 17377;
|
||||
public const uint AddPublishedDataItemsTemplate = 17378;
|
||||
public const uint AddPublishedDataItemsTemplateInputArguments = 17379;
|
||||
public const uint AddPublishedDataItemsTemplateOutputArguments = 17380;
|
||||
public const uint RemovePublishedDataSet = 17384;
|
||||
public const uint RemovePublishedDataSetInputArguments = 17385;
|
||||
public const uint AddDataSetFolder = 17398;
|
||||
public const uint AddDataSetFolderInputArguments = 17401;
|
||||
public const uint AddDataSetFolderOutputArguments = 17402;
|
||||
public const uint RemoveDataSetFolder = 17403;
|
||||
public const uint RemoveDataSetFolderInputArguments = 17404;
|
||||
}
|
||||
|
||||
private sealed class PubSubKeyPushTargetRegistration
|
||||
{
|
||||
public PubSubKeyPushTargetRegistration(
|
||||
|
||||
@@ -1,464 +1,470 @@
|
||||
/* ========================================================================
|
||||
* 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 NUnit.Framework;
|
||||
using Opc.Ua.PubSub.Application;
|
||||
using Opc.Ua.PubSub.Tests;
|
||||
using Opc.Ua.PubSub.Transports;
|
||||
using Opc.Ua.Tests;
|
||||
|
||||
namespace Opc.Ua.PubSub.Server.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
[TestSpec("9.1.3.4", Summary = "AddConnection handler")]
|
||||
[TestSpec("9.1.3.5", Summary = "RemoveConnection handler")]
|
||||
[TestSpec("9.1.6", Summary = "Configuration methods")]
|
||||
[TestSpec("9.1.4.3", Summary = "PublishedDataItemsType variable methods")]
|
||||
[TestSpec("9.1.4.5", Summary = "DataSetFolderType dataset methods")]
|
||||
public class PubSubMethodHandlersMutationTests
|
||||
{
|
||||
[Test]
|
||||
[TestSpec("9.1.3.4")]
|
||||
public void OnAddConnection_ValidInput_ReturnsGoodAndNodeId()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var connCfg = new PubSubConnectionDataType
|
||||
{
|
||||
Name = "handler-conn",
|
||||
TransportProfileUri =
|
||||
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
|
||||
Address = new ExtensionObject(
|
||||
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
|
||||
};
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnAddConnection(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
Assert.That(outputs[0].TryGetValue(out NodeId id), Is.True);
|
||||
Assert.That(id.IsNull, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.3.5")]
|
||||
public void OnRemoveConnection_ValidInput_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var connCfg = new PubSubConnectionDataType
|
||||
{
|
||||
Name = "to-remove",
|
||||
TransportProfileUri =
|
||||
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
|
||||
Address = new ExtensionObject(
|
||||
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
|
||||
};
|
||||
ArrayOf<Variant> addInputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
|
||||
var addOutputs = new List<Variant>();
|
||||
handlers.OnAddConnection(
|
||||
BuildContext(), method: null!, inputArguments: addInputs, outputArguments: addOutputs);
|
||||
Assert.That(addOutputs[0].TryGetValue(out NodeId connId), Is.True);
|
||||
|
||||
ArrayOf<Variant> removeInputs = BuildArray(Variant.From(connId));
|
||||
var removeOutputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnRemoveConnection(
|
||||
BuildContext(), method: null!, inputArguments: removeInputs,
|
||||
outputArguments: removeOutputs);
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.6")]
|
||||
public void OnGetConfiguration_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnGetConfiguration(
|
||||
BuildContext(), method: null!, inputArguments: default, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.6")]
|
||||
public void OnSetConfiguration_ValidInput_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var cfg = new PubSubConfigurationDataType
|
||||
{
|
||||
Connections = [],
|
||||
PublishedDataSets = []
|
||||
};
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(cfg)));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnSetConfiguration(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedDataItems_RegistersPublishedDataItemsDataSet()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedDataItems(
|
||||
BuildContext(),
|
||||
method: null!,
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("items"),
|
||||
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(3));
|
||||
Assert.That(configuration.PublishedDataSets, Has.Count.EqualTo(1));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedDataItemsTemplate_UsesProvidedMetaData()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
var metaData = new DataSetMetaDataType
|
||||
{
|
||||
Name = "template",
|
||||
Fields = new ArrayOf<FieldMetaData>(new[]
|
||||
{
|
||||
new FieldMetaData
|
||||
{
|
||||
Name = "FromTemplate",
|
||||
DataType = DataTypeIds.DateTime,
|
||||
ValueRank = ValueRanks.Scalar
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedDataItemsTemplate(
|
||||
BuildContext(),
|
||||
method: null!,
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("template"),
|
||||
Variant.From(new ExtensionObject(metaData)),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(2));
|
||||
Assert.That(configuration.PublishedDataSets[0].Name, Is.EqualTo("template"));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedEvents_RegistersPublishedEventsDataSet()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var selectedField = new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
||||
BrowsePath = new ArrayOf<QualifiedName>(new[] { new QualifiedName(BrowseNames.EventId) }),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedEvents(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "AddVariables"),
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("events"),
|
||||
Variant.From(ObjectIds.Server),
|
||||
Variant.From(new ArrayOf<string>(s_eventIdAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<SimpleAttributeOperand>(
|
||||
new[] { selectedField })),
|
||||
Variant.From(new ExtensionObject(new ContentFilter()))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(3));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedEventsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.3")]
|
||||
public void OnAddVariablesAndRemoveVariables_MutateFieldsAndBumpConfigurationVersion()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var addDataSetOutputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
handlers.OnAddPublishedDataItems(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("items"),
|
||||
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: addDataSetOutputs);
|
||||
|
||||
var addedVariable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_StartTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
var addVariableOutputs = new List<Variant>();
|
||||
ServiceResult addResult = handlers.OnAddVariables(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "AddVariables"),
|
||||
inputArguments: BuildArray(
|
||||
addDataSetOutputs[1],
|
||||
Variant.From(new ArrayOf<string>(s_startTimeAlias)),
|
||||
Variant.From(s_notPromotedField),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { addedVariable }))),
|
||||
outputArguments: addVariableOutputs);
|
||||
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
|
||||
Assert.That(addVariableOutputs, Has.Count.EqualTo(2));
|
||||
var removeVariableOutputs = new List<Variant>();
|
||||
ServiceResult removeResult = handlers.OnRemoveVariables(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
|
||||
inputArguments: BuildArray(addVariableOutputs[0], Variant.From(new ArrayOf<uint>(s_firstVariableIndex))),
|
||||
outputArguments: removeVariableOutputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? items), Is.True);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
|
||||
Assert.That(StatusCode.IsGood(removeResult.StatusCode), Is.True);
|
||||
Assert.That(items!.PublishedData, Has.Count.EqualTo(1));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetMetaData.ConfigurationVersion.MinorVersion,
|
||||
Is.GreaterThan(1u));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.5")]
|
||||
public void OnAddDataSetFolder_ReturnsGoodWithNodeId()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From("my-folder"));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnAddDataSetFolder(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
private static PubSubMethodHandlers CreateHandlers()
|
||||
{
|
||||
return CreateHandlers(CreateApplication());
|
||||
}
|
||||
|
||||
private static PubSubMethodHandlers CreateHandlers(IPubSubApplication app)
|
||||
{
|
||||
var options = new PubSubServerOptions
|
||||
{
|
||||
ExposeConfigurationMethods = true
|
||||
};
|
||||
return new PubSubMethodHandlers(
|
||||
app, null, options, NUnitTelemetryContext.Create());
|
||||
}
|
||||
|
||||
private static IPubSubApplication CreateApplication()
|
||||
{
|
||||
return new PubSubApplicationBuilder(
|
||||
NUnitTelemetryContext.Create())
|
||||
.WithApplicationId("handler-mutation-tests")
|
||||
.UseConfiguration(new PubSubConfigurationDataType
|
||||
{
|
||||
Connections = [],
|
||||
PublishedDataSets = []
|
||||
})
|
||||
.UseAllStandardEncoders()
|
||||
.AddTransportFactory(new StubTransportFactory())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static SystemContext BuildContext()
|
||||
{
|
||||
return new SystemContext(NUnitTelemetryContext.Create());
|
||||
}
|
||||
|
||||
private static ArrayOf<Variant> BuildArray(params Variant[] values)
|
||||
{
|
||||
return new ArrayOf<Variant>(values);
|
||||
}
|
||||
|
||||
private static MethodState NewPublishedDataItemsMethod(string dataSetName, string methodName)
|
||||
{
|
||||
var parent = new BaseObjectState(null)
|
||||
{
|
||||
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}", 0),
|
||||
BrowseName = new QualifiedName(dataSetName)
|
||||
};
|
||||
var method = new MethodState(parent)
|
||||
{
|
||||
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}:{methodName}", 0),
|
||||
BrowseName = new QualifiedName(methodName)
|
||||
};
|
||||
parent.AddChild(method);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static readonly string[] s_currentTimeAlias = ["CurrentTime"];
|
||||
private static readonly string[] s_eventIdAlias = ["EventId"];
|
||||
private static readonly string[] s_startTimeAlias = ["StartTime"];
|
||||
private static readonly bool[] s_notPromotedField = [false];
|
||||
private static readonly uint[] s_firstVariableIndex = [0u];
|
||||
|
||||
private sealed class StubTransportFactory : IPubSubTransportFactory
|
||||
{
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public IPubSubTransport Create(
|
||||
PubSubConnectionDataType connection,
|
||||
ITelemetryContext telemetry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_ = connection;
|
||||
_ = telemetry;
|
||||
_ = timeProvider;
|
||||
return new StubTransport();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubTransport : IPubSubTransport
|
||||
{
|
||||
private bool m_isConnected;
|
||||
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive;
|
||||
|
||||
public bool IsConnected => m_isConnected;
|
||||
|
||||
public event EventHandler<PubSubTransportStateChangedEventArgs>? StateChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ValueTask OpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
m_isConnected = true;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask CloseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
m_isConnected = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask SendAsync(
|
||||
ReadOnlyMemory<byte> payload,
|
||||
string? topic = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = payload;
|
||||
_ = topic;
|
||||
_ = cancellationToken;
|
||||
return default;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
return TestAsyncEnumerable.Empty<PubSubTransportFrame>();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
m_isConnected = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ========================================================================
|
||||
* 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 NUnit.Framework;
|
||||
using Opc.Ua.PubSub.Application;
|
||||
using Opc.Ua.PubSub.Tests;
|
||||
using Opc.Ua.PubSub.Transports;
|
||||
using Opc.Ua.Tests;
|
||||
|
||||
namespace Opc.Ua.PubSub.Server.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
[TestSpec("9.1.3.4", Summary = "AddConnection handler")]
|
||||
[TestSpec("9.1.3.5", Summary = "RemoveConnection handler")]
|
||||
[TestSpec("9.1.6", Summary = "Configuration methods")]
|
||||
[TestSpec("9.1.4.3", Summary = "PublishedDataItemsType variable methods")]
|
||||
[TestSpec("9.1.4.5", Summary = "DataSetFolderType dataset methods")]
|
||||
public class PubSubMethodHandlersMutationTests
|
||||
{
|
||||
[Test]
|
||||
[TestSpec("9.1.3.4")]
|
||||
public void OnAddConnection_ValidInput_ReturnsGoodAndNodeId()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var connCfg = new PubSubConnectionDataType
|
||||
{
|
||||
Name = "handler-conn",
|
||||
TransportProfileUri =
|
||||
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
|
||||
Address = new ExtensionObject(
|
||||
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
|
||||
};
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnAddConnection(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
Assert.That(outputs[0].TryGetValue(out NodeId id), Is.True);
|
||||
Assert.That(id.IsNull, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.3.5")]
|
||||
public void OnRemoveConnection_ValidInput_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var connCfg = new PubSubConnectionDataType
|
||||
{
|
||||
Name = "to-remove",
|
||||
TransportProfileUri =
|
||||
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
|
||||
Address = new ExtensionObject(
|
||||
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
|
||||
};
|
||||
ArrayOf<Variant> addInputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
|
||||
var addOutputs = new List<Variant>();
|
||||
handlers.OnAddConnection(
|
||||
BuildContext(), method: null!, inputArguments: addInputs, outputArguments: addOutputs);
|
||||
Assert.That(addOutputs[0].TryGetValue(out NodeId connId), Is.True);
|
||||
|
||||
ArrayOf<Variant> removeInputs = BuildArray(Variant.From(connId));
|
||||
var removeOutputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnRemoveConnection(
|
||||
BuildContext(), method: null!, inputArguments: removeInputs,
|
||||
outputArguments: removeOutputs);
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.6")]
|
||||
public void OnGetConfiguration_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnGetConfiguration(
|
||||
BuildContext(), method: null!, inputArguments: default, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.6")]
|
||||
public void OnSetConfiguration_ValidInput_ReturnsGood()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
var cfg = new PubSubConfigurationDataType
|
||||
{
|
||||
Connections = [],
|
||||
PublishedDataSets = []
|
||||
};
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(cfg)));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnSetConfiguration(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedDataItems_RegistersPublishedDataItemsDataSet()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedDataItems(
|
||||
BuildContext(),
|
||||
method: null!,
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("items"),
|
||||
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(3));
|
||||
Assert.That(configuration.PublishedDataSets, Has.Count.EqualTo(1));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedDataItemsTemplate_UsesProvidedMetaData()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
var metaData = new DataSetMetaDataType
|
||||
{
|
||||
Name = "template",
|
||||
Fields = new ArrayOf<FieldMetaData>(new[]
|
||||
{
|
||||
new FieldMetaData
|
||||
{
|
||||
Name = "FromTemplate",
|
||||
DataType = DataTypeIds.DateTime,
|
||||
ValueRank = ValueRanks.Scalar
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedDataItemsTemplate(
|
||||
BuildContext(),
|
||||
method: null!,
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("template"),
|
||||
Variant.From(new ExtensionObject(metaData)),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(2));
|
||||
Assert.That(configuration.PublishedDataSets[0].Name, Is.EqualTo("template"));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.5")]
|
||||
public void OnAddPublishedEvents_RegistersPublishedEventsDataSet()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var outputs = new List<Variant>();
|
||||
var selectedField = new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
||||
BrowsePath = new ArrayOf<QualifiedName>(new[] { new QualifiedName(BrowseNames.EventId) }),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
|
||||
ServiceResult result = handlers.OnAddPublishedEvents(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "AddVariables"),
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("events"),
|
||||
Variant.From(ObjectIds.Server),
|
||||
Variant.From(new ArrayOf<string>(s_eventIdAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<SimpleAttributeOperand>(
|
||||
new[] { selectedField })),
|
||||
Variant.From(new ExtensionObject(new ContentFilter()))),
|
||||
outputArguments: outputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(2));
|
||||
Assert.That(outputs[0].TryGetValue(out ExtensionObject configurationVersion), Is.True);
|
||||
Assert.That(
|
||||
configurationVersion.TryGetValue(out ConfigurationVersionDataType? _),
|
||||
Is.True);
|
||||
Assert.That(outputs[1].TryGetValue(out NodeId dataSetId), Is.True);
|
||||
Assert.That(dataSetId.IsNull, Is.False);
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedEventsDataType? _), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.4.3")]
|
||||
public void OnAddVariablesAndRemoveVariables_MutateFieldsAndBumpConfigurationVersion()
|
||||
{
|
||||
IPubSubApplication application = CreateApplication();
|
||||
PubSubMethodHandlers handlers = CreateHandlers(application);
|
||||
var addDataSetOutputs = new List<Variant>();
|
||||
var variable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
handlers.OnAddPublishedDataItems(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
|
||||
inputArguments: BuildArray(
|
||||
Variant.From("items"),
|
||||
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
|
||||
Variant.From(Array.Empty<DataSetFieldFlags>()),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { variable }))),
|
||||
outputArguments: addDataSetOutputs);
|
||||
|
||||
var addedVariable = new PublishedVariableDataType
|
||||
{
|
||||
PublishedVariable = new NodeId(Variables.Server_ServerStatus_StartTime),
|
||||
AttributeId = Attributes.Value
|
||||
};
|
||||
var addVariableOutputs = new List<Variant>();
|
||||
ServiceResult addResult = handlers.OnAddVariables(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "AddVariables"),
|
||||
inputArguments: BuildArray(
|
||||
addDataSetOutputs[1],
|
||||
Variant.From(new ArrayOf<string>(s_startTimeAlias)),
|
||||
Variant.From(s_notPromotedField),
|
||||
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
|
||||
new[] { addedVariable }))),
|
||||
outputArguments: addVariableOutputs);
|
||||
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
|
||||
Assert.That(addVariableOutputs, Has.Count.EqualTo(2));
|
||||
var removeVariableOutputs = new List<Variant>();
|
||||
ServiceResult removeResult = handlers.OnRemoveVariables(
|
||||
BuildContext(),
|
||||
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
|
||||
inputArguments: BuildArray(addVariableOutputs[0], Variant.From(new ArrayOf<uint>(s_firstVariableIndex))),
|
||||
outputArguments: removeVariableOutputs);
|
||||
|
||||
PubSubConfigurationDataType configuration = application.GetConfiguration();
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
|
||||
out PublishedDataItemsDataType? items), Is.True);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
|
||||
Assert.That(StatusCode.IsGood(removeResult.StatusCode), Is.True);
|
||||
Assert.That(items!.PublishedData, Has.Count.EqualTo(1));
|
||||
Assert.That(configuration.PublishedDataSets[0].DataSetMetaData.ConfigurationVersion.MinorVersion,
|
||||
Is.GreaterThan(1u));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestSpec("9.1.5")]
|
||||
public void OnAddDataSetFolder_ReturnsGoodWithNodeId()
|
||||
{
|
||||
PubSubMethodHandlers handlers = CreateHandlers();
|
||||
ArrayOf<Variant> inputs = BuildArray(Variant.From("my-folder"));
|
||||
var outputs = new List<Variant>();
|
||||
ServiceResult result = handlers.OnAddDataSetFolder(
|
||||
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
|
||||
Assert.That(outputs, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
private static PubSubMethodHandlers CreateHandlers()
|
||||
{
|
||||
return CreateHandlers(CreateApplication());
|
||||
}
|
||||
|
||||
private static PubSubMethodHandlers CreateHandlers(IPubSubApplication app)
|
||||
{
|
||||
var options = new PubSubServerOptions
|
||||
{
|
||||
ExposeConfigurationMethods = true
|
||||
};
|
||||
return new PubSubMethodHandlers(
|
||||
app, null, options, NUnitTelemetryContext.Create());
|
||||
}
|
||||
|
||||
private static IPubSubApplication CreateApplication()
|
||||
{
|
||||
return new PubSubApplicationBuilder(
|
||||
NUnitTelemetryContext.Create())
|
||||
.WithApplicationId("handler-mutation-tests")
|
||||
.UseConfiguration(new PubSubConfigurationDataType
|
||||
{
|
||||
Connections = [],
|
||||
PublishedDataSets = []
|
||||
})
|
||||
.UseAllStandardEncoders()
|
||||
.AddTransportFactory(new StubTransportFactory())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static SystemContext BuildContext()
|
||||
{
|
||||
return new SystemContext(NUnitTelemetryContext.Create());
|
||||
}
|
||||
|
||||
private static ArrayOf<Variant> BuildArray(params Variant[] values)
|
||||
{
|
||||
return new ArrayOf<Variant>(values);
|
||||
}
|
||||
|
||||
private static MethodState NewPublishedDataItemsMethod(string dataSetName, string methodName)
|
||||
{
|
||||
var parent = new BaseObjectState(null)
|
||||
{
|
||||
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}", 0),
|
||||
BrowseName = new QualifiedName(dataSetName)
|
||||
};
|
||||
var method = new MethodState(parent)
|
||||
{
|
||||
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}:{methodName}", 0),
|
||||
BrowseName = new QualifiedName(methodName)
|
||||
};
|
||||
parent.AddChild(method);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static readonly string[] s_currentTimeAlias = ["CurrentTime"];
|
||||
private static readonly string[] s_eventIdAlias = ["EventId"];
|
||||
private static readonly string[] s_startTimeAlias = ["StartTime"];
|
||||
private static readonly bool[] s_notPromotedField = [false];
|
||||
private static readonly uint[] s_firstVariableIndex = [0u];
|
||||
|
||||
private sealed class StubTransportFactory : IPubSubTransportFactory
|
||||
{
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public IPubSubTransport Create(
|
||||
PubSubConnectionDataType connection,
|
||||
ITelemetryContext telemetry,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_ = connection;
|
||||
_ = telemetry;
|
||||
_ = timeProvider;
|
||||
return new StubTransport();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubTransport : IPubSubTransport
|
||||
{
|
||||
private bool m_isConnected;
|
||||
|
||||
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
|
||||
|
||||
public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive;
|
||||
|
||||
public bool IsConnected => m_isConnected;
|
||||
|
||||
public event EventHandler<PubSubTransportStateChangedEventArgs>? StateChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ValueTask OpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
m_isConnected = true;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask CloseAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
m_isConnected = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask SendAsync(
|
||||
ReadOnlyMemory<byte> payload,
|
||||
string? topic = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = payload;
|
||||
_ = topic;
|
||||
_ = cancellationToken;
|
||||
return default;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
return TestAsyncEnumerable.Empty<PubSubTransportFrame>();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
m_isConnected = false;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,8 +722,8 @@ namespace Opc.Ua.PubSub.Server.Tests
|
||||
RemoveSecurityGroupMethod = NewMethod(15447);
|
||||
AddPushTargetMethod = NewMethod(25441);
|
||||
RemovePushTargetMethod = NewMethod(25444);
|
||||
AddDataSetFolderMethod = NewMethod(16884);
|
||||
RemoveDataSetFolderMethod = NewMethod(16923);
|
||||
AddDataSetFolderMethod = NewMethod(PublishedDataSetsNodeIds.AddDataSetFolder);
|
||||
RemoveDataSetFolderMethod = NewMethod(PublishedDataSetsNodeIds.RemoveDataSetFolder);
|
||||
StatusVariable = new BaseDataVariableState(null)
|
||||
{
|
||||
NodeId = new NodeId(17406u),
|
||||
@@ -736,7 +736,7 @@ namespace Opc.Ua.PubSub.Server.Tests
|
||||
};
|
||||
PublishedDataSetsObject = new BaseObjectState(PublishSubscribeObject)
|
||||
{
|
||||
NodeId = new NodeId(14478u),
|
||||
NodeId = ObjectIds.PublishSubscribe_PublishedDataSets,
|
||||
BrowseName = new QualifiedName("PublishedDataSets")
|
||||
};
|
||||
SecurityGroupsObject = new BaseObjectState(PublishSubscribeObject)
|
||||
@@ -762,14 +762,16 @@ namespace Opc.Ua.PubSub.Server.Tests
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(15447u))).Returns(RemoveSecurityGroupMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(25441u))).Returns(AddPushTargetMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(25444u))).Returns(RemovePushTargetMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(16884u))).Returns(AddDataSetFolderMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(new NodeId(16923u))).Returns(RemoveDataSetFolderMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(
|
||||
new NodeId(PublishedDataSetsNodeIds.AddDataSetFolder))).Returns(AddDataSetFolderMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<MethodState>(
|
||||
new NodeId(PublishedDataSetsNodeIds.RemoveDataSetFolder))).Returns(RemoveDataSetFolderMethod);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseVariableState>(new NodeId(17406u))).Returns(StatusVariable);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseVariableState>(It.IsAny<NodeId>()))
|
||||
.Returns((NodeId id) => id == new NodeId(17406u) ? StatusVariable : null!);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(ObjectIds.PublishSubscribe))
|
||||
.Returns(PublishSubscribeObject);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(new NodeId(14478u)))
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(ObjectIds.PublishSubscribe_PublishedDataSets))
|
||||
.Returns(PublishedDataSetsObject);
|
||||
diagnosticsNm.Setup(m => m.FindPredefinedNode<BaseObjectState>(new NodeId(15443u)))
|
||||
.Returns(SecurityGroupsObject);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
/* ========================================================================
|
||||
* 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/
|
||||
* ======================================================================*/
|
||||
|
||||
namespace Opc.Ua.PubSub.Server.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// The standard NodeIds of the methods of <c>PublishSubscribe.PublishedDataSets</c>
|
||||
/// (i=17371) and of their argument variables, as declared by the OPC UA NodeSet.
|
||||
/// The tests keep their own copy so that they check the NodeIds the node manager
|
||||
/// assigns against the standard, not against its own constants. The
|
||||
/// <c>Placeholder*</c> ids are the method declarations of the DataSetFolderType
|
||||
/// <c><DataSetFolderName></c> placeholder, which must not be bound.
|
||||
/// </summary>
|
||||
internal static class PublishedDataSetsNodeIds
|
||||
{
|
||||
public const uint AddPublishedDataItems = 17372;
|
||||
public const uint AddPublishedDataItemsInputArguments = 17373;
|
||||
public const uint AddPublishedDataItemsOutputArguments = 17374;
|
||||
public const uint AddPublishedEvents = 17375;
|
||||
public const uint AddPublishedEventsInputArguments = 17376;
|
||||
public const uint AddPublishedEventsOutputArguments = 17377;
|
||||
public const uint AddPublishedDataItemsTemplate = 17378;
|
||||
public const uint AddPublishedDataItemsTemplateInputArguments = 17379;
|
||||
public const uint AddPublishedDataItemsTemplateOutputArguments = 17380;
|
||||
public const uint RemovePublishedDataSet = 17384;
|
||||
public const uint RemovePublishedDataSetInputArguments = 17385;
|
||||
public const uint AddDataSetFolder = 17398;
|
||||
public const uint AddDataSetFolderInputArguments = 17401;
|
||||
public const uint AddDataSetFolderOutputArguments = 17402;
|
||||
public const uint RemoveDataSetFolder = 17403;
|
||||
public const uint RemoveDataSetFolderInputArguments = 17404;
|
||||
public const uint PlaceholderAddPublishedDataItems = 14479;
|
||||
public const uint PlaceholderAddDataSetFolder = 16884;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user