/* ======================================================================== * 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. * ======================================================================*/ using System.Xml; namespace Opc.Ua.Aot.Tests { /// /// Executes reference storage contracts directly without managed reflection or test hooks. /// [ClassDataSource(Shared = SharedType.PerTestSession)] public sealed class NodeStateReferenceAotTests(AotTestFixture fixture) { /// /// Pins original-object snapshots, indexed browse ordering and removal across all measured degrees. /// [Test] [Arguments(0)] [Arguments(1)] [Arguments(2)] [Arguments(4)] [Arguments(8)] [Arguments(16)] [Arguments(128)] [Arguments(1024)] public async Task ReferencePopulationPreservesIdentityIndexesAndSnapshotsAsync(int count) { var context = new SystemContext(fixture.Telemetry); var node = new BaseObjectState(null); var input = new List(); var oracle = new ReferenceDictionary(); for (int index = 0; index < count; index++) { ExpandedNodeId target = index % 2 == 0 ? new NodeId((uint)(50000 + index), 2) : new ExpandedNodeId(new NodeId((uint)(50000 + index)), "urn:reference:native"); var reference = new NodeStateReference(ReferenceTypeIds.HasComponent, index % 3 == 0, target); input.Add(reference); oracle.Add(reference, 0); } node.AddReferences(input); var snapshot = new List(); node.GetReferences(context, snapshot); await Assert.That(snapshot.Count).IsEqualTo(count); for (int index = 0; index < count; index++) { IReference reference = input[index]; await Assert.That(ReferenceEquals(snapshot[index], reference)).IsTrue(); await Assert.That(node.ReferenceExists( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)).IsTrue(); await Assert.That(node.AddReferenceIfMissing( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)).IsFalse(); } foreach (bool inverse in new[] { false, true }) { using INodeBrowser browser = node.CreateBrowser( context, null, ReferenceTypeIds.HasComponent, false, inverse ? BrowseDirection.Inverse : BrowseDirection.Forward, default, null, false); foreach (IReference expected in oracle.Find(ReferenceTypeIds.HasComponent, inverse)) { await Assert.That(ReferenceEquals(browser.Next(), expected)).IsTrue(); } await Assert.That(browser.Next()).IsNull(); } var clone = (BaseObjectState)node.Clone(); for (int index = count - 1; index >= 0; index--) { IReference reference = input[index]; await Assert.That(node.RemoveReference( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)).IsTrue(); await Assert.That(node.ReferenceExists( reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)).IsFalse(); } var remaining = new List(); node.GetReferences(context, remaining); await Assert.That(remaining.Count).IsEqualTo(0); clone.GetReferences(context, remaining); await Assert.That(remaining.Count).IsEqualTo(count); for (int index = 0; index < count; index++) { await Assert.That(ReferenceEquals(remaining[index], input[index])).IsTrue(); await Assert.That(ReferenceEquals(snapshot[index], input[index])).IsTrue(); } } /// /// Pins mutable insertion keys and node Target identity through copy and reference-stream encoding. /// [Test] [Arguments(false)] [Arguments(true)] public async Task MutableReferencesAndTargetsPreserveCopyAndEncodingContractsAsync(bool xml) { var context = new SystemContext(fixture.Telemetry); var messageContext = ServiceMessageContext.CreateEmpty(fixture.Telemetry); var node = new BaseObjectState(null); var mutable = new ReferenceNode(ReferenceTypeIds.Organizes, false, new NodeId(500u, 2)); var target = new BaseObjectState(null) { NodeId = new NodeId(700u, 2) }; var targetReference = new NodeStateReference(ReferenceTypeIds.HasComponent, false, target); var remote = new NodeStateReference(ReferenceTypeIds.HasProperty, false, new ExpandedNodeId(new NodeId(900u), "urn:reference:remote", 1)); node.AddReferences([mutable, targetReference, remote]); mutable.ReferenceTypeId = ReferenceTypeIds.HasProperty; mutable.IsInverse = true; mutable.TargetId = new NodeId(501u, 2); target.NodeId = new NodeId(701u, 2); var clone = (BaseObjectState)node.Clone(); await Assert.That(node.ReferenceExists(ReferenceTypeIds.Organizes, false, new NodeId(500u, 2))).IsTrue(); await Assert.That(node.ReferenceExists(ReferenceTypeIds.HasProperty, true, new NodeId(501u, 2))).IsFalse(); await Assert.That(clone.ReferenceExists(ReferenceTypeIds.HasProperty, true, new NodeId(501u, 2))).IsTrue(); await Assert.That(clone.ReferenceExists(ReferenceTypeIds.Organizes, false, new NodeId(500u, 2))).IsFalse(); var copied = new List(); clone.GetReferences(context, copied); await Assert.That(copied.Count).IsEqualTo(3); await Assert.That(ReferenceEquals(copied[0], mutable)).IsTrue(); await Assert.That(ReferenceEquals(copied[1], targetReference)).IsTrue(); await Assert.That(ReferenceEquals(targetReference.Target, target)).IsTrue(); await Assert.That(targetReference.TargetId).IsEqualTo(new ExpandedNodeId(new NodeId(700u, 2))); var loaded = new BaseObjectState(null); if (xml) { using var encoder = new XmlEncoder(messageContext); encoder.Push("Root", Namespaces.OpcUaXsd); node.SaveReferences(context, encoder); encoder.Pop(); using var text = new StringReader(encoder.CloseAndReturnText()); using XmlReader reader = XmlReader.Create(text, CoreUtils.DefaultXmlReaderSettings()); using var decoder = new XmlDecoder(null, reader, messageContext); loaded.UpdateReferences(context, decoder); } else { using var stream = new MemoryStream(); using (var encoder = new BinaryEncoder(stream, messageContext, true)) { node.SaveReferences(context, encoder); } stream.Position = 0; using var decoder = new BinaryDecoder(stream, messageContext, true); loaded.UpdateReferences(context, decoder); await Assert.That(stream.Position).IsEqualTo(stream.Length); } var decoded = new List(); loaded.GetReferences(context, decoded); await Assert.That(decoded.Count).IsEqualTo(3); await Assert.That(decoded[0].ReferenceTypeId).IsEqualTo(ReferenceTypeIds.HasProperty); await Assert.That(decoded[0].IsInverse).IsTrue(); await Assert.That(decoded[0].TargetId).IsEqualTo(new ExpandedNodeId(new NodeId(501u, 2))); await Assert.That(decoded[1].TargetId).IsEqualTo(new ExpandedNodeId(new NodeId(700u, 2))); await Assert.That(((NodeStateReference)decoded[1]).Target).IsNull(); await Assert.That(decoded[2].TargetId.NamespaceUri).IsEqualTo("urn:reference:remote"); await Assert.That(decoded[2].TargetId.ServerIndex).IsEqualTo(1u); await Assert.That(decoded[2].TargetId.InnerNodeId).IsEqualTo(new NodeId(900u)); await Assert.That(ReferenceEquals(decoded[0], mutable)).IsFalse(); await Assert.That(loaded.ReferenceExists( ReferenceTypeIds.HasProperty, true, new NodeId(501u, 2))).IsTrue(); } } }