Skip to content

Commit 61ba003

Browse files
marcschierCopilot
andauthored
Add integration test for server side complex types (#3961) (#3970)
# Description Adds a client↔server **integration test** for the server side complex type system (`ServerComplexTypeSystem.LoadComplexTypesAsync` / the `AddComplexTypeSystem` builder extension). Until now this feature was only covered by mock-based unit tests (`ServerComplexTypeSystemTests`); there was no end-to-end test proving a real server that hosts runtime-loaded custom DataTypes (no compiled .NET backing) can encode/decode them over the wire. The new test (`Tests/Opc.Ua.Server.Tests/ComplexTypes/`): - A `ReferenceServer` subclass hosts a small hand-authored **NodeSet2** (embedded resource) loaded at runtime by a test node manager. It declares custom types with no compiled backing: - `TestPoint` — a `Structure` (`X:Int32`, `Y:Int32`, `Name:String`) with a Default Binary encoding node. - `TestColor` — an `Enumeration` (Red/Green/Blue). - The server builds dynamic stand-in encodeables via the default `OnNodeManagerStartedAsync` path and then populates the sample variables (the runtime struct value is built through `server.Factory.TryGetEncodeableType(...).CreateInstance()`). - A real client connects, loads its complex type system, and exercises the round-trip. **Assertions:** 1. The server `IEncodeableFactory` registers the runtime `TestPoint` stand-in (+ its binary encoding id) and the `TestColor` enumeration. 2. Server→client read: the client decodes the runtime structure (`X`/`Y`/`Name`). 3. Enum read round-trips. 4. Client→server write: the server decodes the runtime structure via its stand-in and returns the updated value. This is a **test-only** change — no production code is modified. ## Related Issues - Fixes #3961 ## Checklist - [ ] 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 the affected tests locally against .net **framework (net48)** and .net **10**, and all passed (4/4 on each TFM). - [ ] 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 Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent d7cb170 commit 61ba003

6 files changed

Lines changed: 747 additions & 0 deletions
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/* ========================================================================
2+
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
3+
*
4+
* OPC Foundation MIT License 1.00
5+
*
6+
* Permission is hereby granted, free of charge, to any person
7+
* obtaining a copy of this software and associated documentation
8+
* files (the "Software"), to deal in the Software without
9+
* restriction, including without limitation the rights to use,
10+
* copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the
12+
* Software is furnished to do so, subject to the following
13+
* conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be
16+
* included in all copies or substantial portions of the Software.
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22+
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24+
* OTHER DEALINGS IN THE SOFTWARE.
25+
*
26+
* The complete license agreement can be found here:
27+
* http://opcfoundation.org/License/MIT/1.00/
28+
* ======================================================================*/
29+
30+
using System;
31+
using System.Globalization;
32+
using System.IO;
33+
using System.Threading.Tasks;
34+
using NUnit.Framework;
35+
using Opc.Ua.Client;
36+
using Opc.Ua.Client.ComplexTypes;
37+
using Opc.Ua.Client.TestFramework;
38+
using Opc.Ua.Encoders;
39+
using Opc.Ua.Server.TestFramework;
40+
using Opc.Ua.Tests;
41+
42+
namespace Opc.Ua.Server.Tests
43+
{
44+
/// <summary>
45+
/// Integration tests for the server side complex type system. A server
46+
/// hosts custom DataTypes that were loaded from a NodeSet at runtime and
47+
/// have no compiled .NET backing; the server builds dynamic stand-in
48+
/// encodeables for them. A real client then reads and writes values of
49+
/// those types over the wire, exercising the full server encode / decode
50+
/// round-trip (issue #3961).
51+
/// </summary>
52+
[TestFixture]
53+
[Category("Server")]
54+
[Category("ComplexTypes")]
55+
[SetCulture("en-us")]
56+
[SetUICulture("en-us")]
57+
[NonParallelizable]
58+
public sealed class ServerComplexTypeSystemIntegrationTests
59+
{
60+
private ITelemetryContext m_telemetry;
61+
private ServerFixture<ServerComplexTypesTestServer> m_serverFixture;
62+
private ClientFixture m_clientFixture;
63+
private ServerComplexTypesTestServer m_server;
64+
private Opc.Ua.Client.ISession m_session;
65+
private string m_pkiRoot;
66+
67+
/// <summary>
68+
/// Starts a server that hosts the runtime-loaded complex types, connects
69+
/// a client and loads the client complex type system.
70+
/// </summary>
71+
[OneTimeSetUp]
72+
public async Task OneTimeSetUpAsync()
73+
{
74+
m_telemetry = NUnitTelemetryContext.Create();
75+
m_pkiRoot = Path.GetTempPath() + Path.GetRandomFileName();
76+
77+
m_serverFixture = new ServerFixture<ServerComplexTypesTestServer>(
78+
t => new ServerComplexTypesTestServer(t))
79+
{
80+
UriScheme = Utils.UriSchemeOpcTcp,
81+
SecurityNone = true,
82+
AutoAccept = true,
83+
OperationLimits = true
84+
};
85+
86+
m_server = await m_serverFixture.StartAsync(m_pkiRoot).ConfigureAwait(false);
87+
88+
m_clientFixture = new ClientFixture(m_telemetry);
89+
await m_clientFixture.LoadClientConfigurationAsync(m_pkiRoot).ConfigureAwait(false);
90+
91+
var url = new Uri(
92+
Utils.UriSchemeOpcTcp +
93+
"://localhost:" +
94+
m_serverFixture.Port.ToString(CultureInfo.InvariantCulture));
95+
96+
try
97+
{
98+
m_session = await m_clientFixture
99+
.ConnectAsync(url, SecurityPolicies.Basic256Sha256)
100+
.ConfigureAwait(false);
101+
}
102+
catch (Exception e)
103+
{
104+
Assert.Ignore(
105+
$"OneTimeSetup failed to create session, tests skipped. Error: {e.Message}");
106+
}
107+
108+
// Load the client complex type system so the runtime types are
109+
// registered in the session factory and read values decode correctly.
110+
var clientTypeSystem = ComplexTypeSystem.Create(m_session, m_telemetry);
111+
clientTypeSystem.DisableDataTypeDictionary = true;
112+
bool loaded = await clientTypeSystem.LoadAsync(false, true).ConfigureAwait(false);
113+
Assert.That(loaded, Is.True, "the client complex type system should load");
114+
}
115+
116+
/// <summary>
117+
/// Closes the session and stops the server.
118+
/// </summary>
119+
[OneTimeTearDown]
120+
public async Task OneTimeTearDownAsync()
121+
{
122+
if (m_session != null)
123+
{
124+
await m_session.CloseAsync().ConfigureAwait(false);
125+
m_session.Dispose();
126+
m_session = null;
127+
}
128+
129+
m_clientFixture?.Dispose();
130+
m_server?.Dispose();
131+
132+
if (m_serverFixture != null)
133+
{
134+
await m_serverFixture.StopAsync().ConfigureAwait(false);
135+
}
136+
137+
try
138+
{
139+
if (!string.IsNullOrEmpty(m_pkiRoot) && Directory.Exists(m_pkiRoot))
140+
{
141+
Directory.Delete(m_pkiRoot, recursive: true);
142+
}
143+
}
144+
catch
145+
{
146+
// best-effort cleanup
147+
}
148+
}
149+
150+
/// <summary>
151+
/// The server must build and register stand-in encodeables for the
152+
/// runtime-loaded structure and enumeration DataTypes.
153+
/// </summary>
154+
[Test]
155+
[Order(100)]
156+
public void ServerRegistersRuntimeStandInTypes()
157+
{
158+
IServerInternal server = m_server.CurrentInstance;
159+
ushort namespaceIndex = ServerNamespaceIndex(server);
160+
161+
ExpandedNodeId structureTypeId = NodeId.ToExpandedNodeId(
162+
new NodeId(ServerComplexTypesTestNodeManager.TestPointDataType, namespaceIndex),
163+
server.NamespaceUris);
164+
ExpandedNodeId binaryEncodingId = NodeId.ToExpandedNodeId(
165+
new NodeId(ServerComplexTypesTestNodeManager.TestPointBinaryEncoding, namespaceIndex),
166+
server.NamespaceUris);
167+
ExpandedNodeId enumTypeId = NodeId.ToExpandedNodeId(
168+
new NodeId(ServerComplexTypesTestNodeManager.TestColorDataType, namespaceIndex),
169+
server.NamespaceUris);
170+
171+
Assert.Multiple(() =>
172+
{
173+
Assert.That(
174+
server.Factory.TryGetEncodeableType(structureTypeId, out IEncodeableType structType),
175+
Is.True,
176+
"the runtime TestPoint structure should be registered in the server factory");
177+
Assert.That(structType, Is.Not.Null);
178+
179+
Assert.That(
180+
server.Factory.TryGetEncodeableType(binaryEncodingId, out _),
181+
Is.True,
182+
"the TestPoint binary encoding id should resolve to the structure stand-in");
183+
184+
Assert.That(
185+
server.Factory.TryGetEnumeratedType(enumTypeId, out IEnumeratedType enumType),
186+
Is.True,
187+
"the runtime TestColor enumeration should be registered in the server factory");
188+
Assert.That(enumType, Is.Not.Null);
189+
});
190+
}
191+
192+
/// <summary>
193+
/// A client reads the runtime structure variable; the server encodes it
194+
/// via its stand-in and the client decodes it into a structured value.
195+
/// </summary>
196+
[Test]
197+
[Order(200)]
198+
public async Task ClientReadsRuntimeStructureValue()
199+
{
200+
NodeId nodeId = ClientNodeId(ServerComplexTypesTestNodeManager.PointValueVariable);
201+
202+
DataValue dataValue = await m_session.ReadValueAsync(nodeId).ConfigureAwait(false);
203+
Assert.That(dataValue.IsNull, Is.False);
204+
Assert.That(StatusCode.IsGood(dataValue.StatusCode), Is.True);
205+
206+
IStructure point = ReadStructure(dataValue);
207+
Assert.Multiple(() =>
208+
{
209+
Assert.That(point["X"].TryGetValue(out int x), Is.True);
210+
Assert.That(x, Is.EqualTo(3));
211+
Assert.That(point["Y"].TryGetValue(out int y), Is.True);
212+
Assert.That(y, Is.EqualTo(4));
213+
Assert.That(point["Name"].TryGetValue(out string name), Is.True);
214+
Assert.That(name, Is.EqualTo("origin"));
215+
});
216+
}
217+
218+
/// <summary>
219+
/// A client reads the runtime enumeration variable and gets the raw
220+
/// enumeration value.
221+
/// </summary>
222+
[Test]
223+
[Order(300)]
224+
public async Task ClientReadsRuntimeEnumValue()
225+
{
226+
NodeId nodeId = ClientNodeId(ServerComplexTypesTestNodeManager.ColorValueVariable);
227+
228+
DataValue dataValue = await m_session.ReadValueAsync(nodeId).ConfigureAwait(false);
229+
Assert.That(dataValue.IsNull, Is.False);
230+
Assert.That(StatusCode.IsGood(dataValue.StatusCode), Is.True);
231+
232+
// TestColor.Green == 1
233+
Assert.That(dataValue.WrappedValue.TryGetValue(out int color), Is.True);
234+
Assert.That(color, Is.EqualTo(1));
235+
}
236+
237+
/// <summary>
238+
/// A client writes a modified runtime structure value; the server
239+
/// decodes it via its stand-in, stores it, and returns the updated value
240+
/// on the next read.
241+
/// </summary>
242+
[Test]
243+
[Order(400)]
244+
public async Task ClientWritesRuntimeStructureValue()
245+
{
246+
NodeId nodeId = ClientNodeId(ServerComplexTypesTestNodeManager.PointValueVariable);
247+
248+
DataValue dataValue = await m_session.ReadValueAsync(nodeId).ConfigureAwait(false);
249+
IStructure point = ReadStructure(dataValue);
250+
251+
point["X"] = new Variant(7);
252+
point["Y"] = new Variant(8);
253+
point["Name"] = new Variant("updated");
254+
255+
ArrayOf<WriteValue> writeValues =
256+
[
257+
new WriteValue
258+
{
259+
NodeId = nodeId,
260+
AttributeId = Attributes.Value,
261+
Value = new DataValue(dataValue.WrappedValue)
262+
}
263+
];
264+
265+
WriteResponse response = await m_session
266+
.WriteAsync(null, writeValues, default)
267+
.ConfigureAwait(false);
268+
Assert.That(response.Results.IsNull, Is.False);
269+
Assert.That(StatusCode.IsGood(response.Results[0]), Is.True,
270+
"the server should decode and accept the runtime structure value");
271+
272+
DataValue readBack = await m_session.ReadValueAsync(nodeId).ConfigureAwait(false);
273+
IStructure updated = ReadStructure(readBack);
274+
Assert.Multiple(() =>
275+
{
276+
Assert.That(updated["X"].TryGetValue(out int x), Is.True);
277+
Assert.That(x, Is.EqualTo(7));
278+
Assert.That(updated["Y"].TryGetValue(out int y), Is.True);
279+
Assert.That(y, Is.EqualTo(8));
280+
Assert.That(updated["Name"].TryGetValue(out string name), Is.True);
281+
Assert.That(name, Is.EqualTo("updated"));
282+
});
283+
}
284+
285+
/// <summary>
286+
/// Extracts the decoded structured value from a read <see cref="DataValue"/>.
287+
/// </summary>
288+
private static IStructure ReadStructure(DataValue dataValue)
289+
{
290+
Assert.That(dataValue.WrappedValue.TryGetValue(out ExtensionObject extensionObject), Is.True);
291+
Assert.That(extensionObject.TryGetValue(out IEncodeable encodeable), Is.True);
292+
var structure = encodeable as IStructure;
293+
Assert.That(structure, Is.Not.Null, "the decoded value should be a complex structure");
294+
return structure;
295+
}
296+
297+
/// <summary>
298+
/// Resolves a node id in the test namespace on the client side.
299+
/// </summary>
300+
private NodeId ClientNodeId(uint identifier)
301+
{
302+
return new NodeId(
303+
identifier,
304+
(ushort)m_session.NamespaceUris.GetIndex(
305+
ServerComplexTypesTestNodeManager.NamespaceUri));
306+
}
307+
308+
/// <summary>
309+
/// Resolves the test namespace index on the server side.
310+
/// </summary>
311+
private static ushort ServerNamespaceIndex(IServerInternal server)
312+
{
313+
return (ushort)server.NamespaceUris.GetIndex(
314+
ServerComplexTypesTestNodeManager.NamespaceUri);
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)