/* ======================================================================== * 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.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using NUnit.Framework; namespace Opc.Ua.Fuzzing { /// /// Pins the deterministic, populated messages used by the encoder corpus. /// [TestFixture] [Category("Fuzzing")] public sealed class EncoderCorpusTests { [Test] public void MessageEncoderCatalogContainsAllSevenSeeds() { string[] names = Testcases.GetMessageEncoders().Select(encoder => encoder.Method.Name).ToArray(); Assert.That(names, Is.EqualTo(s_seedNames)); Assert.That(Testcases.SeedTimestamp, Is.EqualTo(new DateTimeUtc(2025, 1, 2, 3, 4, 5))); } [Test] public void SharedMessageEncoderCatalogContainsTheFourCanonicalMessages() { Assert.That( Testcases.MessageEncoders.Select(encoder => encoder.Method.Name), Is.EqualTo(EncoderTestMessages.RichNames.ToArray())); } [TestCase("ReadRequest")] [TestCase("ReadResponse")] [TestCase("PublishResponse")] [TestCase("WriteRequest")] public void SharedCanonicalGeneratorsRejectMissingEncoders(string canonicalName) { Testcases.MessageEncoder encode = Testcases.MessageEncoders.Single( encoder => encoder.Method.Name == canonicalName); Assert.That( () => encode(null!), Throws.TypeOf().With.Property("ParamName").EqualTo("encoder")); } [TestCaseSource(nameof(PersistentRichCorpusCases))] public void SharedCanonicalGeneratorsMatchTheRichCorpus(string seedName, string wire) { string canonicalName = seedName.EndsWith("Rich", StringComparison.Ordinal) ? seedName[..^4] : seedName; Testcases.MessageEncoder encode = Testcases.MessageEncoders.Single( encoder => encoder.Method.Name == canonicalName); ByteString actual = EncodeCallback(encode, wire); Assert.That(actual, Is.EqualTo(EncodeSeed(seedName, wire))); EncoderTestMessages.AssertPopulated(EncoderTestMessages.Decode(actual.Span, wire)!); } [TestCase("ReadRequest")] [TestCase("ReadResponse")] [TestCase("PublishResponse")] [TestCase("WriteRequest")] public void RichSeedFactoriesReturnIndependentPopulatedMessages(string messageName) { IEncodeable first = EncoderTestMessages.Create(messageName); IEncodeable second = EncoderTestMessages.Create(messageName); Assert.That(first, Is.Not.SameAs(second)); EncoderTestMessages.AssertPopulated(first); EncoderTestMessages.AssertPopulated(second); Assert.That(Utils.IsEqual(first, second), Is.True); switch (first) { case ReadRequest read: read.NodesToRead[2].IndexRange = "4:5"; break; case ReadResponse read: read.ResponseHeader.ServiceDiagnostics!.AdditionalInfo = "changed"; break; case PublishResponse publish: Assert.That( publish.NotificationMessage.NotificationData[0] .TryGetValue(out DataChangeNotification? notification), Is.True); notification!.MonitoredItems[0].ClientHandle = 999; break; case WriteRequest write: write.NodesToWrite[0].IndexRange = "4:5"; break; default: throw new ArgumentException("Unknown rich message.", nameof(messageName)); } Assert.That(Utils.IsEqual(first, second), Is.False, "Nested seed objects must not be shared."); EncoderTestMessages.AssertPopulated(second); } [TestCaseSource(nameof(DeterministicEncodingCases))] public void FreshCorpusMessagesHaveDeterministicEncodingAndPopulatedDecodes(string seedName, string wire) { ByteString first = EncodeSeed(seedName, wire); ByteString second = EncodeSeed(seedName, wire); IEncodeable decoded = EncoderTestMessages.Decode(first.Span, wire)!; IEncodeable decodedAgain = EncoderTestMessages.Decode(second.Span, wire)!; Assert.That(first.IsNull, Is.False); Assert.That(first.Length, Is.GreaterThan(64), "Each seed must cross the default segment boundary."); Assert.That(second, Is.EqualTo(first), "Freshly constructed seeds must have stable bytes/text."); Assert.That(decoded, Is.Not.Null); Assert.That(decodedAgain, Is.Not.SameAs(decoded)); EncoderTestMessages.AssertPopulated(decoded!); EncoderTestMessages.AssertPopulated(decodedAgain!); Assert.That(Utils.IsEqual(decoded, decodedAgain), Is.True); Assert.That(EncoderTestMessages.Encode(decoded, wire), Is.EqualTo(first)); } [TestCaseSource(nameof(PersistentRichCorpusCases))] public async Task CheckedInRichCorpusMatchesTheDeterministicGeneratorAsync(string seedName, string wire) { string extension = wire == "Binary" ? "bin" : wire.ToLowerInvariant(); string path = Path.Combine( AppContext.BaseDirectory, "Testcases", seedName.ToLowerInvariant() + "." + extension); using var source = File.OpenRead(path); using var buffer = new MemoryStream(); await source.CopyToAsync(buffer).ConfigureAwait(false); ByteString actual = ByteString.From(buffer.ToArray()); // XmlWriter indents with Environment.NewLine, so generated XML is CRLF on // Windows and LF on Linux. The corpus is stored byte-for-byte (see // .gitattributes), so compare XML with newlines normalized while binary and // JSON stay exact. if (wire == "Xml") { Assert.That( NormalizeNewlines(actual), Is.EqualTo(NormalizeNewlines(EncodeSeed(seedName, wire)))); } else { Assert.That(actual, Is.EqualTo(EncodeSeed(seedName, wire))); } IEncodeable decoded = EncoderTestMessages.Decode(actual.Span, wire)!; EncoderTestMessages.AssertPopulated(decoded!); } private static string NormalizeNewlines(ByteString value) { return Encoding.UTF8.GetString(value.Span.ToArray()) .Replace("\r\n", "\n", StringComparison.Ordinal); } private static IEnumerable DeterministicEncodingCases() { foreach (string name in s_seedNames) { foreach (string wire in new[] { "Binary", "Json", "Xml" }) { yield return new TestCaseData(name, wire); } } } private static IEnumerable PersistentRichCorpusCases() { foreach (TestCaseData test in DeterministicEncodingCases()) { if (test.Arguments[0] is "ReadRequestRich" or "ReadResponseRich" or "PublishResponse" or "WriteRequestRich") { yield return test; } } } private static ByteString EncodeSeed(string seedName, string wire) { Testcases.MessageEncoder encode = Testcases.GetMessageEncoders() .Single(encoder => encoder.Method.Name == seedName); return EncodeCallback(encode, wire); } private static ByteString EncodeCallback(Testcases.MessageEncoder encode, string wire) { using var stream = new MemoryStream(); switch (wire) { case "Binary": using (var encoder = new BinaryEncoder(FuzzableCode.MessageContext)) { encode(encoder); return ByteString.From(encoder.CloseAndReturnBuffer()); } case "Json": using (var encoder = new JsonEncoder( stream, FuzzableCode.MessageContext, JsonEncoderOptions.Verbose)) { encode(encoder); encoder.Close(); return ByteString.From(stream.ToArray()); } case "Xml": using (var encoder = new XmlEncoder(FuzzableCode.MessageContext)) { encoder.SetMappingTables( FuzzableCode.MessageContext.NamespaceUris, FuzzableCode.MessageContext.ServerUris); encode(encoder); return ByteString.From(Encoding.UTF8.GetBytes(encoder.CloseAndReturnText()!)); } default: throw new ArgumentException("Unknown wire encoding.", nameof(wire)); } } private static readonly string[] s_seedNames = [ "ReadRequestRich", "ReadResponseRich", "PublishResponse", "BrowseRequest", "WriteRequestRich", "DataTypeNodeMessage", "VariableNodeMessage" ]; } /// /// Shared concrete-value oracles; none of these helpers has a fuzz callback signature. /// internal static class EncoderTestMessages { internal static ArrayOf RichNames { get; } = [ "ReadRequest", "ReadResponse", "PublishResponse", "WriteRequest" ]; internal static IEncodeable Create(string messageName) { return messageName switch { "ReadRequest" => Testcases.CreateRichReadRequest(), "ReadResponse" => Testcases.CreateRichReadResponse(), "PublishResponse" => Testcases.CreatePublishResponse(), "WriteRequest" => Testcases.CreateRichWriteRequest(), _ => throw new ArgumentException("Unknown rich message.", nameof(messageName)) }; } internal static ByteString Encode(IEncodeable message, string wire) { switch (wire) { case "Binary": return ByteString.From(BinaryEncoder.EncodeMessage(message, FuzzableCode.MessageContext)); case "Json": return ByteString.From(Encoding.UTF8.GetBytes( FuzzableCode.EncodeJsonMessage(message, JsonEncoderOptions.Verbose))); case "Xml": using (var encoder = new XmlEncoder(FuzzableCode.MessageContext)) { WriteTypedXmlMessage(encoder, message); return ByteString.From(Encoding.UTF8.GetBytes(encoder.CloseAndReturnText()!)); } default: throw new ArgumentException("Unknown wire encoding.", nameof(wire)); } } internal static IEncodeable? Decode(ReadOnlySpan input, string wire, bool strict = true) { using var stream = new MemoryStream(input.ToArray()); return wire switch { "Binary" => FuzzableCode.FuzzBinaryDecoderCore(stream, strict), "Json" => FuzzableCode.FuzzJsonDecoderCore(Encoding.UTF8.GetString(input.ToArray()), strict), "Xml" => FuzzableCode.FuzzXmlDecoderCore(stream, strict), _ => throw new ArgumentException("Unknown wire encoding.", nameof(wire)) }; } internal static void AssertPopulated(IEncodeable message) { Assert.That(message, Is.Not.Null, "A swallowed source-decoding failure is not a successful replay."); switch (message) { case ReadRequest request: AssertReadRequest(request); break; case ReadResponse response: AssertReadResponse(response); break; case PublishResponse publish: AssertPublishResponse(publish); break; case WriteRequest write: AssertWriteRequest(write); break; case BrowseRequest browse: Assert.That(browse.RequestedMaxReferencesPerNode, Is.EqualTo(10)); Assert.That(browse.NodesToBrowse.Count, Is.EqualTo(1)); Assert.That(browse.NodesToBrowse[0].NodeId, Is.EqualTo(ObjectIds.ObjectsFolder)); Assert.That(browse.NodesToBrowse[0].IncludeSubtypes, Is.True); Assert.That(browse.NodesToBrowse[0].ResultMask, Is.EqualTo((uint)BrowseResultMask.All)); Assert.That(browse.RequestHeader.Timestamp, Is.EqualTo(s_timestamp)); break; case DataTypeNode node: Assert.That(node.NodeId, Is.EqualTo(new NodeId(1234, 2))); Assert.That(node.BrowseName, Is.EqualTo(new QualifiedName("Issue3546DataType", 2))); Assert.That(node.References.Count, Is.EqualTo(2)); Assert.That(node.RolePermissions.Count, Is.EqualTo(1)); Assert.That(node.DataTypeDefinition.TryGetValue(out StructureDefinition? definition), Is.True); Assert.That(definition!.Fields.Count, Is.EqualTo(2)); Assert.That(definition.Fields[0].DataType, Is.EqualTo(DataTypeIds.UInt32)); Assert.That(definition.Fields[1].DataType, Is.EqualTo(DataTypeIds.String)); break; case VariableNode node: Assert.That(node.NodeId, Is.EqualTo(new NodeId(4321, 2))); Assert.That(node.References.Count, Is.EqualTo(2)); Assert.That(node.ArrayDimensions.Count, Is.EqualTo(2)); Assert.That(node.ArrayDimensions[0], Is.EqualTo(1)); Assert.That(node.ArrayDimensions[1], Is.EqualTo(2)); Assert.That(node.Value.TryGetStructure(out Argument? argument), Is.True); Assert.That(argument!.Name, Is.EqualTo("Sample")); Assert.That(argument.DataType, Is.EqualTo(DataTypeIds.UInt32)); break; default: throw new ArgumentException($"Unexpected message type {message.GetType().Name}.", nameof(message)); } } private static void WriteTypedXmlMessage(XmlEncoder encoder, IEncodeable message) { // Keep a concrete generic type: EncodeMessage emits an IEncodeable root QName. switch (message) { case ReadRequest read: encoder.EncodeMessage(read); break; case ReadResponse read: encoder.EncodeMessage(read); break; case PublishResponse publish: encoder.EncodeMessage(publish); break; case WriteRequest write: encoder.EncodeMessage(write); break; case BrowseRequest browse: encoder.EncodeMessage(browse); break; case DataTypeNode node: encoder.EncodeMessage(node); break; case VariableNode node: encoder.EncodeMessage(node); break; default: throw new ArgumentException("Unknown message type.", nameof(message)); } } private static void AssertReadRequest(ReadRequest request) { Assert.That(request.MaxAge, Is.EqualTo(1000)); Assert.That(request.TimestampsToReturn, Is.EqualTo(TimestampsToReturn.Source)); Assert.That(request.RequestHeader.Timestamp, Is.EqualTo(s_timestamp)); Assert.That(request.RequestHeader.RequestHandle, Is.EqualTo(422)); Assert.That(request.RequestHeader.TimeoutHint, Is.EqualTo(10000)); Assert.That(request.RequestHeader.ReturnDiagnostics, Is.EqualTo((uint)DiagnosticsMasks.All)); AssertAdditionalHeader(request.RequestHeader.AdditionalHeader); Assert.That(request.NodesToRead.Count, Is.EqualTo(6)); Assert.That(request.NodesToRead[0].NodeId, Is.EqualTo(new NodeId(123))); Assert.That(request.NodesToRead[0].AttributeId, Is.EqualTo(Attributes.UserRolePermissions)); Assert.That(request.NodesToRead[1].NodeId, Is.EqualTo(new NodeId(4444, 2))); Assert.That(request.NodesToRead[1].AttributeId, Is.EqualTo(Attributes.Description)); Assert.That(request.NodesToRead[2].NodeId, Is.EqualTo(new NodeId("RevisionCounter", 3))); Assert.That(request.NodesToRead[2].IndexRange, Is.EqualTo("1:2")); Assert.That(request.NodesToRead[2].DataEncoding, Is.EqualTo(new QualifiedName("Default Binary"))); Assert.That( request.NodesToRead[3].NodeId, Is.EqualTo(new NodeId(new Guid("00112233-4455-6677-8899-aabbccddeeff"), 1))); Assert.That(request.NodesToRead[4].AttributeId, Is.EqualTo(Attributes.AccessLevel)); Assert.That( request.NodesToRead[5].NodeId, Is.EqualTo(new NodeId(ByteString.From([66, 22, 55, 44, 11]), 4))); } private static void AssertReadResponse(ReadResponse response) { Assert.That(response.ResponseHeader.Timestamp, Is.EqualTo(s_timestamp)); Assert.That(response.ResponseHeader.RequestHandle, Is.EqualTo(42)); Assert.That(response.ResponseHeader.StringTable.Count, Is.EqualTo(3)); Assert.That(response.ResponseHeader.StringTable[0], Is.EqualTo("Hello")); Assert.That(response.ResponseHeader.StringTable[1], Is.EqualTo("World")); Assert.That(response.ResponseHeader.StringTable[2], Is.EqualTo("Goodbye")); AssertAdditionalHeader(response.ResponseHeader.AdditionalHeader); DiagnosticInfo diagnostic = response.ResponseHeader.ServiceDiagnostics!; Assert.That(diagnostic!.SymbolicId, Is.EqualTo(0)); Assert.That(diagnostic.NamespaceUri, Is.EqualTo(1)); Assert.That(diagnostic.LocalizedText, Is.EqualTo(2)); Assert.That(diagnostic.AdditionalInfo, Is.EqualTo("NodeId not found")); Assert.That( diagnostic.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadAggregateConfigurationRejected)); Assert.That(diagnostic.InnerDiagnosticInfo!.AdditionalInfo, Is.EqualTo("Invalid index range")); Assert.That( diagnostic.InnerDiagnosticInfo.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadIndexRangeInvalid)); Assert.That( diagnostic.InnerDiagnosticInfo.InnerDiagnosticInfo!.AdditionalInfo, Is.EqualTo("Invalid channel")); Assert.That( diagnostic.InnerDiagnosticInfo.InnerDiagnosticInfo.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadSecureChannelIdInvalid)); DiagnosticInfo deepest = diagnostic.InnerDiagnosticInfo.InnerDiagnosticInfo.InnerDiagnosticInfo!; Assert.That(deepest!.AdditionalInfo, Is.EqualTo("Already exists")); Assert.That(deepest.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadAlreadyExists)); Assert.That(deepest.InnerDiagnosticInfo, Is.Null); Assert.That(response.Results.Count, Is.EqualTo(11)); Assert.That(response.Results[0].WrappedValue.TryGetValue(out string greeting), Is.True); Assert.That(greeting, Is.EqualTo("Hello World")); AssertSeedTimestamps(response.Results[0]); Assert.That(response.Results[1].WrappedValue.TryGetValue(out uint number), Is.True); Assert.That(number, Is.EqualTo(12345678)); Assert.That(response.Results[1].StatusCode, Is.EqualTo((StatusCode)StatusCodes.BadDataLost)); Assert.That(response.Results[1].ServerTimestamp, Is.EqualTo(new DateTimeUtc(2025, 1, 2))); Assert.That(response.Results[2].WrappedValue.TryGetValue(out ByteString bytes), Is.True); Assert.That(bytes, Is.EqualTo(ByteString.From([0, 1, 2, 3, 4, 5, 6]))); Assert.That(response.Results[2].ServerTimestamp, Is.EqualTo(DateTimeUtc.MaxValue)); Assert.That(response.Results[3].WrappedValue.TryGetValue(out byte smallNumber), Is.True); Assert.That(smallNumber, Is.EqualTo(42)); Assert.That(response.Results[4].WrappedValue.TryGetValue(out ulong largeNumber), Is.True); Assert.That(largeNumber, Is.EqualTo(0xbadbeefUL)); Assert.That(response.Results[5].WrappedValue.TryGetValue(out MatrixOf matrix), Is.True); Assert.That(matrix.IsNull, Is.False); Assert.That(matrix.Dimensions, Is.EqualTo(s_dimensions)); Assert.That(matrix.Span.ToArray(), Is.EqualTo(s_byteMatrix)); Assert.That(response.Results[6].WrappedValue.TryGetValue(out double real), Is.True); Assert.That(real, Is.EqualTo(2025.111)); Assert.That(response.Results[7].WrappedValue.TryGetValue(out LocalizedText text), Is.True); Assert.That(text, Is.EqualTo(new LocalizedText("en-us", "The text"))); Assert.That(response.Results[8].WrappedValue.TryGetValue(out QualifiedName name), Is.True); Assert.That(name, Is.EqualTo(new QualifiedName("The text", 2))); Assert.That(response.Results[9].WrappedValue.TryGetStructure(out ThreeDVector? vector), Is.True); AssertVector(vector!, 1, -1, 0); Assert.That(response.Results[10].WrappedValue.TryGetValue(out ArrayOf vectors), Is.True); Assert.That(vectors.IsNull, Is.False); Assert.That(vectors.Count, Is.EqualTo(2)); Assert.That(vectors[0].TryGetValue(out ThreeDVector? firstVector), Is.True); AssertVector(firstVector!, 1, -1, 0); Assert.That(vectors[1].TryGetValue(out ThreeDVector? secondVector), Is.True); AssertVector(secondVector!, 2, -2, 1); AssertNestedDiagnostic(response.DiagnosticInfos[0]); } private static void AssertPublishResponse(PublishResponse response) { Assert.That(response.SubscriptionId, Is.EqualTo(1234)); Assert.That(response.MoreNotifications, Is.True); Assert.That(response.AvailableSequenceNumbers.Count, Is.EqualTo(4)); Assert.That(response.AvailableSequenceNumbers[3], Is.EqualTo(4)); Assert.That(response.ResponseHeader.Timestamp, Is.EqualTo(s_timestamp)); Assert.That(response.ResponseHeader.StringTable[0], Is.EqualTo("No error occurred")); Assert.That(response.NotificationMessage.SequenceNumber, Is.EqualTo(123456)); Assert.That(response.NotificationMessage.PublishTime, Is.EqualTo(s_timestamp)); Assert.That(response.NotificationMessage.NotificationData.Count, Is.EqualTo(1)); Assert.That( response.NotificationMessage.NotificationData[0].TryGetValue(out DataChangeNotification? notification), Is.True); Assert.That(notification!.MonitoredItems.Count, Is.EqualTo(11)); Assert.That(notification.MonitoredItems[0].ClientHandle, Is.EqualTo(122)); Assert.That(notification.MonitoredItems[0].Value.WrappedValue.TryGetValue(out string greeting), Is.True); Assert.That(greeting, Is.EqualTo("Hello World")); AssertSeedTimestamps(notification.MonitoredItems[0].Value); Assert.That( notification.MonitoredItems[1].Value.WrappedValue.TryGetValue(out MatrixOf matrix), Is.True); Assert.That(matrix.Dimensions, Is.EqualTo(s_dimensions)); Assert.That(matrix.Span.ToArray(), Is.EqualTo(s_uintMatrix)); Assert.That(notification.MonitoredItems[2].Value.WrappedValue.TryGetValue(out NodeId numericNode), Is.True); Assert.That(numericNode, Is.EqualTo(new NodeId(1000, 2))); Assert.That(notification.MonitoredItems[3].Value.WrappedValue.TryGetValue(out NodeId stringNode), Is.True); Assert.That(stringNode, Is.EqualTo(new NodeId("Counter", 1))); Assert.That(notification.MonitoredItems[4].Value.WrappedValue.TryGetValue(out bool boolean), Is.True); Assert.That(boolean, Is.True); Assert.That(notification.MonitoredItems[5].Value.WrappedValue.TryGetValue(out byte smallNumber), Is.True); Assert.That(smallNumber, Is.EqualTo(123)); Assert.That(notification.MonitoredItems[6].Value.WrappedValue.TryGetValue(out float single), Is.True); Assert.That(single, Is.EqualTo(123.123f)); Assert.That(notification.MonitoredItems[7].Value.WrappedValue.TryGetValue(out double real), Is.True); Assert.That(real, Is.EqualTo(12301232.123)); Assert.That(notification.MonitoredItems[8].Value.WrappedValue.TryGetValue(out long signed), Is.True); Assert.That(signed, Is.EqualTo(-123012321234123L)); Assert.That(notification.MonitoredItems[9].Value.WrappedValue.TryGetValue(out ulong unsigned), Is.True); Assert.That(unsigned, Is.EqualTo(123012321234123UL)); Assert.That( notification.MonitoredItems[10].Value.WrappedValue.TryGetValue(out ArrayOf array), Is.True); Assert.That(array.Count, Is.EqualTo(10)); Assert.That(array[0], Is.EqualTo(1)); Assert.That(array[9], Is.Zero); AssertNestedDiagnostic(notification.DiagnosticInfos[0]); Assert.That(response.Results.Count, Is.EqualTo(2)); Assert.That(response.Results[1], Is.EqualTo((StatusCode)StatusCodes.BadSequenceNumberUnknown)); AssertNestedDiagnostic(response.DiagnosticInfos[0]); } private static void AssertWriteRequest(WriteRequest request) { Assert.That(request.RequestHeader.Timestamp, Is.EqualTo(s_timestamp)); Assert.That(request.RequestHeader.RequestHandle, Is.EqualTo(422)); Assert.That(request.RequestHeader.ReturnDiagnostics, Is.EqualTo((uint)DiagnosticsMasks.All)); Assert.That(request.RequestHeader.AdditionalHeader.IsNull, Is.True); Assert.That(request.NodesToWrite.Count, Is.EqualTo(10)); Assert.That(request.NodesToWrite[0].NodeId, Is.EqualTo(new NodeId(123))); Assert.That(request.NodesToWrite[0].IndexRange, Is.EqualTo("1:2")); Assert.That(request.NodesToWrite[0].Value.WrappedValue.TryGetValue(out string greeting), Is.True); Assert.That(greeting, Is.EqualTo("Hello World")); AssertSeedTimestamps(request.NodesToWrite[0].Value); Assert.That(request.NodesToWrite[1].AttributeId, Is.EqualTo(Attributes.ValueRank)); Assert.That(request.NodesToWrite[1].Value.WrappedValue.TryGetValue(out int number), Is.True); Assert.That(number, Is.EqualTo(12345)); Assert.That(request.NodesToWrite[2].Value.WrappedValue.TryGetValue(out float single), Is.True); Assert.That(single, Is.EqualTo(123.45f)); Assert.That(request.NodesToWrite[3].Value.WrappedValue.TryGetValue(out double real), Is.True); Assert.That(real, Is.EqualTo(123.45)); Assert.That(request.NodesToWrite[4].Value.WrappedValue.TryGetValue(out bool boolean), Is.True); Assert.That(boolean, Is.True); Assert.That(request.NodesToWrite[5].Value.WrappedValue.TryGetValue(out ByteString bytes), Is.True); Assert.That(bytes, Is.EqualTo(ByteString.From([1, 2, 3, 4, 5]))); Assert.That(request.NodesToWrite[6].NodeId, Is.EqualTo(new NodeId("FastCounter", 2))); Assert.That(request.NodesToWrite[6].Value.WrappedValue.TryGetValue(out ArrayOf integers), Is.True); Assert.That(integers.Span.ToArray(), Is.EqualTo(s_integers)); Assert.That( request.NodesToWrite[7].NodeId, Is.EqualTo(new NodeId(ByteString.From([0xaa, 0xbb, 0xcc, 0xdd, 0xee]), 3))); Assert.That(request.NodesToWrite[7].Value.WrappedValue.TryGetValue(out ArrayOf singles), Is.True); Assert.That(singles.Count, Is.EqualTo(3)); Assert.That(singles[2], Is.EqualTo(3.3f)); Assert.That( request.NodesToWrite[8].NodeId, Is.EqualTo(new NodeId(new Guid("10213243-5465-7687-98a9-bacbdcedfe0f"), 1))); Assert.That(request.NodesToWrite[8].Value.WrappedValue.TryGetValue(out ArrayOf doubles), Is.True); Assert.That(doubles.Count, Is.EqualTo(3)); Assert.That(doubles[1], Is.EqualTo(2.2)); Assert.That(request.NodesToWrite[9].Value.WrappedValue.TryGetValue(out ArrayOf strings), Is.True); Assert.That(strings.Span.ToArray(), Is.EqualTo(s_strings)); } private static void AssertAdditionalHeader(ExtensionObject header) { Assert.That(header.IsNull, Is.False); Assert.That(header.TryGetValue(out AdditionalParametersType? parameters), Is.True); Assert.That(parameters!.Parameters.Count, Is.EqualTo(1)); Assert.That(parameters.Parameters[0].Key, Is.EqualTo(new QualifiedName("traceparent"))); Assert.That(parameters.Parameters[0].Value.TryGetValue(out string traceParent), Is.True); Assert.That(traceParent, Is.EqualTo("00-00112233445566778899aabbccddeeff-0123456789abcdef-01")); } private static void AssertSeedTimestamps(in DataValue value) { Assert.That(value.SourceTimestamp, Is.EqualTo(new DateTimeUtc(2025, 1, 2, 3, 5, 5))); Assert.That(value.ServerTimestamp, Is.EqualTo(s_timestamp)); Assert.That(value.SourcePicoseconds, Is.EqualTo(10)); Assert.That(value.ServerPicoseconds, Is.EqualTo(100)); } private static void AssertNestedDiagnostic(DiagnosticInfo diagnostic) { Assert.That(diagnostic.AdditionalInfo, Is.EqualTo("Hello World")); Assert.That(diagnostic.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadCertificateHostNameInvalid)); Assert.That(diagnostic.InnerDiagnosticInfo!.AdditionalInfo, Is.EqualTo("Unknown node")); Assert.That( diagnostic.InnerDiagnosticInfo.InnerStatusCode, Is.EqualTo((StatusCode)StatusCodes.BadNodeIdUnknown)); Assert.That(diagnostic.InnerDiagnosticInfo.InnerDiagnosticInfo, Is.Null); } private static void AssertVector(ThreeDVector vector, double x, double y, double z) { Assert.That(vector.X, Is.EqualTo(x)); Assert.That(vector.Y, Is.EqualTo(y)); Assert.That(vector.Z, Is.EqualTo(z)); } private static readonly DateTimeUtc s_timestamp = new(2025, 1, 2, 3, 4, 5); private static readonly int[] s_dimensions = [2, 2, 2]; private static readonly byte[] s_byteMatrix = [1, 2, 3, 4, 11, 22, 33, 44]; private static readonly uint[] s_uintMatrix = [1, 2, 3, 4, 11, 22, 33, 44]; private static readonly int[] s_integers = [1, 2, 3, 4, 5]; private static readonly string[] s_strings = ["one", "two", "three"]; } }