mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
9b034f8291
# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## 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. - [ ] 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. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
686 lines
32 KiB
C#
686 lines
32 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 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.Reflection;
|
|
using System.Text;
|
|
using System.Xml;
|
|
using System.Xml.Linq;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.SourceGeneration.Generator.Tests
|
|
{
|
|
/// <summary>
|
|
/// D-5: a generated data type with multi-dimensional fields has to be
|
|
/// wire compatible with the DataTypeDefinition driven structure codec
|
|
/// (<see cref="Encoders.Structure"/>, used by the complex type system and
|
|
/// by any other stack) reading the StructureDefinition the type
|
|
/// publishes: matrix fields are inline matrices (OPC 10000-6 5.2.5,
|
|
/// 5.4.5), not Variants, and a field with a single ArrayDimensions entry
|
|
/// is a plain array. Each direction is checked for null, empty and
|
|
/// populated matrices in every encoding.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Generator")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
[Parallelizable]
|
|
public class MatrixFieldInteropTests
|
|
{
|
|
private const string ModelUri = "http://test.org/UA/MX/";
|
|
|
|
public enum Codec
|
|
{
|
|
Binary,
|
|
JsonCompact,
|
|
JsonVerbose,
|
|
XmlDecoder,
|
|
XmlParser
|
|
}
|
|
|
|
public enum Content
|
|
{
|
|
Null,
|
|
Empty,
|
|
Full
|
|
}
|
|
|
|
[OneTimeSetUp]
|
|
public void OneTimeSetUp()
|
|
{
|
|
m_generated = DataTypeModelTests.Generate(Model);
|
|
m_assembly = DataTypeModelTests.Compile(m_generated);
|
|
m_gridsType = m_assembly.GetType("Test.MX.Grids", throwOnError: true)!;
|
|
m_cellType = m_assembly.GetType("Test.MX.Cell", throwOnError: true)!;
|
|
m_colorType = m_assembly.GetType("Test.MX.Color", throwOnError: true)!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The generated class writes matrix fields through the raw
|
|
/// WriteVariantValue / ReadVariantValue pair (no Variant framing) and
|
|
/// a single dimension "matrix" as an array.
|
|
/// </summary>
|
|
[Test]
|
|
public void GeneratedCodeUsesInlineMatrixCalls()
|
|
{
|
|
string code = m_generated
|
|
.Where(f => f.Key.EndsWith("DataTypes.g.cs", StringComparison.Ordinal))
|
|
.Select(f => f.Value)
|
|
.Single();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(code, Does.Contain(
|
|
"global::Opc.Ua.EncoderExtensions.WriteInlineMatrixValue(encoder, \"Doubles\", global::Opc.Ua.Variant.From(Doubles));"));
|
|
Assert.That(code, Does.Contain(
|
|
"Doubles = decoder.ReadVariantValue(\"Doubles\", global::Opc.Ua.TypeInfo.Create(" +
|
|
"global::Opc.Ua.BuiltInType.Double, global::Opc.Ua.ValueRanks.TwoDimensions)).GetDoubleMatrix();"));
|
|
Assert.That(code, Does.Contain(
|
|
"Cube = decoder.ReadVariantValue(\"Cube\", global::Opc.Ua.TypeInfo.Create(" +
|
|
"global::Opc.Ua.BuiltInType.Int32, 3)).GetInt32Matrix();"));
|
|
Assert.That(code, Does.Contain(
|
|
"global::Opc.Ua.BuiltInType.Enumeration, global::Opc.Ua.ValueRanks.TwoDimensions))" +
|
|
".GetEnumerationMatrix<global::Test.MX.Color>();"));
|
|
Assert.That(code, Does.Contain(
|
|
"global::Opc.Ua.BuiltInType.UInt32, global::Opc.Ua.ValueRanks.TwoDimensions)).GetUInt32Matrix();"));
|
|
Assert.That(code, Does.Contain("encoder.WriteEncodeableMatrix(\"Cells\", Cells);"));
|
|
Assert.That(code, Does.Contain("encoder.WriteDoubleArray(\"Row\", Row);"));
|
|
Assert.That(code, Does.Contain("Row = decoder.ReadDoubleArray(\"Row\");"));
|
|
Assert.That(code, Does.Not.Match(@"encoder\.WriteVariant\(""(Doubles|Strings|Colors|Flags|Objects|Values|Cube)"""));
|
|
});
|
|
Assert.That(
|
|
m_gridsType.GetProperty("Row")!.PropertyType,
|
|
Is.EqualTo(typeof(ArrayOf<double>)),
|
|
"a single ArrayDimensions entry is a one dimensional array");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The published StructureDefinition matches what Encode writes.
|
|
/// </summary>
|
|
[Test]
|
|
public void StructureDefinitionDescribesTheFieldsAsWritten()
|
|
{
|
|
StructureDefinition definition = CreateDefinition();
|
|
Dictionary<string, StructureField> fields = definition.Fields.ToArray()!
|
|
.ToDictionary(f => f.Name!);
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(fields["Doubles"].ValueRank, Is.EqualTo(ValueRanks.TwoDimensions));
|
|
Assert.That(fields["Cube"].ValueRank, Is.EqualTo(3));
|
|
Assert.That(fields["Row"].ValueRank, Is.EqualTo(ValueRanks.OneDimension));
|
|
Assert.That(fields["Row"].ArrayDimensions.ToArray(), Is.EqualTo(s_rowDimensions));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// A StructureField ValueRank is -1 or >= 1, never 0 (OPC 10000-3
|
|
/// 8.51): a OneOrMoreDimensions field without ArrayDimensions is
|
|
/// published (and read) as a two dimensional matrix of unknown
|
|
/// lengths.
|
|
/// </summary>
|
|
[Test]
|
|
public void UnrankedMatrixFieldIsPublishedAsTwoDimensions()
|
|
{
|
|
StructureDefinition definition = CreateDefinition("CreateUnranked");
|
|
StructureField field = definition.Fields.ToArray()!.Single(f => f.Name == "M");
|
|
Assert.That(field.ValueRank, Is.EqualTo(ValueRanks.TwoDimensions));
|
|
Assert.That(field.ArrayDimensions.ToArray(), Is.EqualTo(s_unknownTwoDimensions));
|
|
Assert.That(
|
|
CreateDefinition().Fields.ToArray()!.Select(f => f.ValueRank),
|
|
Has.None.EqualTo(ValueRanks.OneOrMoreDimensions));
|
|
|
|
string code = m_generated
|
|
.Where(f => f.Key.EndsWith("DataTypes.g.cs", StringComparison.Ordinal))
|
|
.Select(f => f.Value)
|
|
.Single();
|
|
Assert.That(code, Does.Contain(
|
|
"M = decoder.ReadVariantValue(\"M\", global::Opc.Ua.TypeInfo.Create(" +
|
|
"global::Opc.Ua.BuiltInType.Int32, global::Opc.Ua.ValueRanks.TwoDimensions)).GetInt32Matrix();"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The generated binary encoding is the inline matrix of OPC 10000-6
|
|
/// 5.2.5 Table 28: Int32 dimensions (-1 for null) followed by the
|
|
/// product of the dimensions values, without a length prefix.
|
|
/// </summary>
|
|
[TestCase(Content.Null)]
|
|
[TestCase(Content.Empty)]
|
|
[TestCase(Content.Full)]
|
|
public void GeneratedBinaryIsTheInlineMatrix(Content content)
|
|
{
|
|
IEncodeable original = CreateGrids(content);
|
|
ServiceMessageContext context = CreateGeneratedContext();
|
|
byte[] encoded;
|
|
using (var encoder = new BinaryEncoder(context))
|
|
{
|
|
original.Encode(encoder);
|
|
encoded = encoder.CloseAndReturnBuffer()!;
|
|
}
|
|
|
|
byte[] expected;
|
|
using (var e = new BinaryEncoder(context))
|
|
{
|
|
e.WriteInt32(null, 7);
|
|
switch (content)
|
|
{
|
|
case Content.Null:
|
|
for (int ii = 0; ii < 8; ii++)
|
|
{
|
|
e.WriteInt32(null, -1); // null dimensions, no values
|
|
}
|
|
e.WriteInt32(null, 0); // Row: the generated default is empty
|
|
break;
|
|
case Content.Empty:
|
|
Dimensions(e, 0, 3); // Doubles
|
|
Dimensions(e, 0, 0); // Strings (MatrixOf.Empty)
|
|
Dimensions(e, 2, 0); // Colors
|
|
Dimensions(e, 1, 0); // Flags
|
|
Dimensions(e, 0, 0); // Objects
|
|
Dimensions(e, 0, 2); // Values
|
|
Dimensions(e, 0, 2); // Cells
|
|
Dimensions(e, 0, 0, 0); // Cube
|
|
e.WriteInt32(null, 0); // Row
|
|
break;
|
|
case Content.Full:
|
|
Dimensions(e, 2, 3);
|
|
foreach (double d in s_doubles)
|
|
{
|
|
e.WriteDouble(null, d);
|
|
}
|
|
Dimensions(e, 2, 2);
|
|
e.WriteString(null, "a");
|
|
e.WriteString(null, null);
|
|
e.WriteString(null, "c");
|
|
e.WriteString(null, "d");
|
|
Dimensions(e, 2, 2);
|
|
foreach (int c in s_colors)
|
|
{
|
|
e.WriteInt32(null, c);
|
|
}
|
|
Dimensions(e, 2, 2);
|
|
foreach (uint f in s_flags)
|
|
{
|
|
e.WriteUInt32(null, f);
|
|
}
|
|
Dimensions(e, 1, 2);
|
|
e.WriteExtensionObject(null, new ExtensionObject(Cell(11)));
|
|
e.WriteExtensionObject(null, ExtensionObject.Null);
|
|
Dimensions(e, 2, 1);
|
|
e.WriteVariant(null, Variant.From(1));
|
|
e.WriteVariant(null, Variant.From("x"));
|
|
Dimensions(e, 1, 2);
|
|
e.WriteInt32(null, 1); // Cell.V
|
|
e.WriteInt32(null, 2);
|
|
Dimensions(e, 2, 1, 2);
|
|
foreach (int c in s_cube)
|
|
{
|
|
e.WriteInt32(null, c);
|
|
}
|
|
e.WriteDoubleArray(null, s_row.ToArrayOf());
|
|
break;
|
|
}
|
|
e.WriteInt32(null, 9);
|
|
expected = e.CloseAndReturnBuffer()!;
|
|
}
|
|
|
|
Assert.That(encoded, Is.EqualTo(expected));
|
|
|
|
static void Dimensions(BinaryEncoder encoder, params int[] dimensions)
|
|
{
|
|
encoder.WriteInt32Array(null, dimensions.ToArrayOf());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generated Encode is read by the DataTypeDefinition driven codec,
|
|
/// whose Encode is read back by the generated Decode, losslessly.
|
|
/// </summary>
|
|
[Test]
|
|
[Combinatorial]
|
|
public void GeneratedAndDefinitionDrivenCodecsInteroperate(
|
|
[Values(Codec.Binary, Codec.JsonCompact, Codec.JsonVerbose)] Codec codec,
|
|
[Values] Content content)
|
|
{
|
|
IEncodeable original = CreateGrids(content);
|
|
ServiceMessageContext generatedContext = CreateGeneratedContext();
|
|
ServiceMessageContext runtimeContext = CreateRuntimeContext(out ExpandedNodeId typeId);
|
|
|
|
// generated -> definition driven codec
|
|
object payload = Encode(codec, generatedContext, original, typeId);
|
|
var structure = (IStructure)Decode(codec, runtimeContext, payload, typeId);
|
|
Assert.That(structure, Is.InstanceOf<Encoders.Structure>());
|
|
AssertDefinitionDrivenValues(structure, content);
|
|
|
|
// definition driven codec -> generated
|
|
object payload2 = Encode(codec, runtimeContext, (IEncodeable)structure, typeId);
|
|
IEncodeable decoded = Decode(codec, generatedContext, payload2, typeId);
|
|
Assert.That(decoded.GetType(), Is.EqualTo(m_gridsType));
|
|
Assert.That(decoded.IsEqual(original), Is.True, "generated -> structure -> generated");
|
|
|
|
if (codec == Codec.Binary)
|
|
{
|
|
Assert.That(payload2, Is.EqualTo(payload), "both codecs write the same bytes");
|
|
}
|
|
|
|
// generated round trip
|
|
IEncodeable again = Decode(codec, generatedContext, Encode(codec, generatedContext, original, typeId), typeId);
|
|
Assert.That(again.IsEqual(original), Is.True, "generated round trip");
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DataTypeDefinition driven codec writes every XML field as a
|
|
/// Variant body (e.g. <Before><Int32>7</Int32></Before>),
|
|
/// so a whole structure does not interoperate with generated code in
|
|
/// XML (a separate, pre-existing difference). The matrix fields must
|
|
/// however be written identically by both, and each side must round
|
|
/// trip its own XML.
|
|
/// </summary>
|
|
[Test]
|
|
[Combinatorial]
|
|
public void XmlMatrixFieldsMatchDefinitionDrivenCodec(
|
|
[Values(Codec.XmlDecoder, Codec.XmlParser)] Codec codec,
|
|
[Values] Content content)
|
|
{
|
|
IEncodeable original = CreateGrids(content);
|
|
ServiceMessageContext generatedContext = CreateGeneratedContext();
|
|
ServiceMessageContext runtimeContext = CreateRuntimeContext(out ExpandedNodeId typeId);
|
|
IEncodeable structure = Decode(
|
|
Codec.Binary,
|
|
runtimeContext,
|
|
Encode(Codec.Binary, generatedContext, original, typeId),
|
|
typeId);
|
|
|
|
string generatedXml = (string)Encode(codec, generatedContext, original, typeId);
|
|
string runtimeXml = (string)Encode(codec, runtimeContext, structure, typeId);
|
|
|
|
XElement generatedRoot = XElement.Parse(generatedXml);
|
|
XElement runtimeRoot = XElement.Parse(runtimeXml);
|
|
XNamespace ns = ModelUri;
|
|
foreach (string field in s_matrixFields)
|
|
{
|
|
XElement generatedField = generatedRoot.Element(ns + field)!;
|
|
XElement runtimeField = runtimeRoot.Element(ns + field)!;
|
|
Assert.That(generatedField, Is.Not.Null, field);
|
|
Assert.That(
|
|
XNode.DeepEquals(generatedField, runtimeField),
|
|
Is.True,
|
|
field + ":\n" + generatedField + "\n" + runtimeField);
|
|
}
|
|
|
|
// The Dimensions of an XML Matrix must be greater than zero (OPC
|
|
// 10000-6 5.3.1.17): an empty matrix field is written as null,
|
|
// which is equivalent to an empty array (5.1.11).
|
|
IEncodeable expected = original;
|
|
IEncodeable expectedStructure = structure;
|
|
if (content == Content.Empty)
|
|
{
|
|
expected = CreateGrids(Content.Null);
|
|
Set(expected, "Row", ArrayOf.Empty<double>());
|
|
expectedStructure = Decode(
|
|
Codec.Binary,
|
|
runtimeContext,
|
|
Encode(Codec.Binary, generatedContext, expected, typeId),
|
|
typeId);
|
|
}
|
|
|
|
IEncodeable decoded = Decode(codec, generatedContext, generatedXml, typeId);
|
|
Assert.That(decoded.IsEqual(expected), Is.True, "generated round trip");
|
|
IEncodeable decodedStructure = Decode(codec, runtimeContext, runtimeXml, typeId);
|
|
Assert.That(decodedStructure.IsEqual(expectedStructure), Is.True, "definition driven round trip");
|
|
}
|
|
|
|
private void AssertDefinitionDrivenValues(IStructure structure, Content content)
|
|
{
|
|
Assert.That(structure["Before"].GetInt32(), Is.EqualTo(7));
|
|
Assert.That(structure["After"].GetInt32(), Is.EqualTo(9));
|
|
|
|
MatrixOf<double> doubles = structure["Doubles"].GetDoubleMatrix();
|
|
MatrixOf<int> cube = structure["Cube"].GetInt32Matrix();
|
|
ArrayOf<double> row = structure["Row"].GetDoubleArray();
|
|
MatrixOf<int> colors = structure["Colors"].GetInt32Matrix();
|
|
MatrixOf<uint> flags = structure["Flags"].GetUInt32Matrix();
|
|
MatrixOf<Variant> values = structure["Values"].GetVariantMatrix();
|
|
MatrixOf<ExtensionObject> objects = structure["Objects"].GetExtensionObjectMatrix();
|
|
switch (content)
|
|
{
|
|
case Content.Null:
|
|
Assert.That(doubles.IsNull, Is.True);
|
|
Assert.That(cube.IsNull, Is.True);
|
|
Assert.That(colors.IsNull, Is.True);
|
|
Assert.That(flags.IsNull, Is.True);
|
|
Assert.That(values.IsNull, Is.True);
|
|
Assert.That(objects.IsNull, Is.True);
|
|
break;
|
|
case Content.Empty:
|
|
Assert.That(doubles.IsNull, Is.False);
|
|
Assert.That(doubles.Dimensions, Is.EqualTo(s_emptyRows));
|
|
Assert.That(cube.Dimensions, Is.EqualTo(s_emptyCube));
|
|
Assert.That(row.IsNull, Is.False);
|
|
Assert.That(row.Count, Is.Zero);
|
|
Assert.That(colors.Dimensions, Is.EqualTo(s_emptyColumns));
|
|
Assert.That(values.Count, Is.Zero);
|
|
Assert.That(objects.Count, Is.Zero);
|
|
break;
|
|
case Content.Full:
|
|
Assert.That(doubles.Dimensions, Is.EqualTo(s_twoByThree));
|
|
Assert.That(doubles.Span.ToArray(), Is.EqualTo(s_doubles));
|
|
Assert.That(cube.Dimensions, Is.EqualTo(s_cubeDimensions));
|
|
Assert.That(cube.Span.ToArray(), Is.EqualTo(s_cube));
|
|
Assert.That(row.ToArray(), Is.EqualTo(s_row));
|
|
Assert.That(colors.Span.ToArray(), Is.EqualTo(s_colors));
|
|
Assert.That(flags.Span.ToArray(), Is.EqualTo(s_flags));
|
|
Assert.That(values.Span[1].GetString(), Is.EqualTo("x"));
|
|
Assert.That(objects.Dimensions, Is.EqualTo(s_oneByTwo));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private IEncodeable CreateGrids(Content content)
|
|
{
|
|
var grids = (IEncodeable)Activator.CreateInstance(m_gridsType)!;
|
|
Set(grids!, "Before", 7);
|
|
Set(grids, "After", 9);
|
|
switch (content)
|
|
{
|
|
case Content.Null:
|
|
break;
|
|
case Content.Empty:
|
|
Set(grids, "Doubles", MatrixOf.From<double>(new double[0, 3]));
|
|
Set(grids, "Strings", MatrixOf<string>.Empty);
|
|
Set(grids, "Colors", MatrixFrom(m_colorType, Array.CreateInstance(m_colorType, 2, 0)));
|
|
Set(grids, "Flags", MatrixOf.From<uint>(new uint[1, 0]));
|
|
Set(grids, "Objects", MatrixOf.From<ExtensionObject>(new ExtensionObject[0, 0]));
|
|
Set(grids, "Values", MatrixOf.From<Variant>(new Variant[0, 2]));
|
|
Set(grids, "Cells", MatrixFrom(m_cellType, Array.CreateInstance(m_cellType, 0, 2)));
|
|
Set(grids, "Cube", MatrixOf.From<int>(new int[0, 0, 0]));
|
|
Set(grids, "Row", ArrayOf.Empty<double>());
|
|
break;
|
|
case Content.Full:
|
|
Set(grids, "Doubles", MatrixOf.From<double>(new double[,] { { 1.5, 2.5, 3.5 }, { 4.5, 5.5, 6.5 } }));
|
|
Set(grids, "Strings", MatrixOf.From<string>(new string[,] { { "a", null! }, { "c", "d" } }));
|
|
Array colors = Array.CreateInstance(m_colorType, 2, 2);
|
|
colors.SetValue(Enum.ToObject(m_colorType, 0), 0, 0);
|
|
colors.SetValue(Enum.ToObject(m_colorType, 1), 0, 1);
|
|
colors.SetValue(Enum.ToObject(m_colorType, 2), 1, 0);
|
|
colors.SetValue(Enum.ToObject(m_colorType, 1), 1, 1);
|
|
Set(grids, "Colors", MatrixFrom(m_colorType, colors));
|
|
Set(grids, "Flags", MatrixOf.From<uint>(new uint[,] { { 1, 2 }, { 3, 0 } }));
|
|
Set(grids, "Objects", MatrixOf.From<ExtensionObject>(new ExtensionObject[,]
|
|
{
|
|
{ new ExtensionObject(Cell(11)), ExtensionObject.Null }
|
|
}));
|
|
Set(grids, "Values", MatrixOf.From<Variant>(new Variant[,]
|
|
{
|
|
{ Variant.From(1) },
|
|
{ Variant.From("x") }
|
|
}));
|
|
Array cells = Array.CreateInstance(m_cellType, 1, 2);
|
|
cells.SetValue(Cell(1), 0, 0);
|
|
cells.SetValue(Cell(2), 0, 1);
|
|
Set(grids, "Cells", MatrixFrom(m_cellType, cells));
|
|
Set(grids, "Cube", MatrixOf.From<int>(new int[,,] { { { 1, 2 } }, { { 3, 4 } } }));
|
|
Set(grids, "Row", new double[] { 1, 2, 3, 4, 5 }.ToArrayOf());
|
|
break;
|
|
}
|
|
return grids;
|
|
}
|
|
|
|
private IEncodeable Cell(int value)
|
|
{
|
|
var cell = (IEncodeable)Activator.CreateInstance(m_cellType)!;
|
|
Set(cell!, "V", value);
|
|
return cell;
|
|
}
|
|
|
|
private static object MatrixFrom(Type elementType, Array array)
|
|
{
|
|
return typeof(MatrixOf)
|
|
.GetMethod(nameof(MatrixOf.From), BindingFlags.Public | BindingFlags.Static)!
|
|
.MakeGenericMethod(elementType)
|
|
.Invoke(null, [array])!;
|
|
}
|
|
|
|
private static void Set(object instance, string name, object value)
|
|
{
|
|
instance.GetType().GetProperty(name)!.SetValue(instance, value);
|
|
}
|
|
|
|
private StructureDefinition CreateDefinition(string factory = "CreateGrids")
|
|
{
|
|
MethodInfo create = m_assembly.GetTypes()
|
|
.Select(t => t.GetMethod(factory, BindingFlags.Public | BindingFlags.Static))
|
|
.First(m => m != null)!;
|
|
return (StructureDefinition)create!.Invoke(null, [CreateNamespaceTable()])!;
|
|
}
|
|
|
|
private static NamespaceTable CreateNamespaceTable()
|
|
{
|
|
var namespaceUris = new NamespaceTable();
|
|
namespaceUris.Append(ModelUri);
|
|
return namespaceUris;
|
|
}
|
|
|
|
private ServiceMessageContext CreateGeneratedContext()
|
|
{
|
|
ServiceMessageContext context = ServiceMessageContext.CreateEmpty(NUnitTelemetryContext.Create());
|
|
context.NamespaceUris = CreateNamespaceTable();
|
|
context.Factory.Builder.AddEncodeableTypes(m_assembly).Commit();
|
|
return context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A context where Grids is the DataTypeDefinition driven structure,
|
|
/// built the way the complex type system builds it: the built-in type
|
|
/// of every field is what the field's DataType resolves to.
|
|
/// </summary>
|
|
private ServiceMessageContext CreateRuntimeContext(out ExpandedNodeId typeId)
|
|
{
|
|
var template = (IEncodeable)Activator.CreateInstance(m_gridsType)!;
|
|
typeId = template!.TypeId;
|
|
var fieldTypes = new Dictionary<string, BuiltInType>
|
|
{
|
|
["Before"] = BuiltInType.Int32,
|
|
["Doubles"] = BuiltInType.Double,
|
|
["Strings"] = BuiltInType.String,
|
|
["Colors"] = BuiltInType.Enumeration,
|
|
["Flags"] = BuiltInType.UInt32,
|
|
["Objects"] = BuiltInType.ExtensionObject,
|
|
["Values"] = BuiltInType.Variant,
|
|
["Cells"] = BuiltInType.Null,
|
|
["Cube"] = BuiltInType.Int32,
|
|
["Row"] = BuiltInType.Double,
|
|
["After"] = BuiltInType.Int32
|
|
};
|
|
var structure = new Encoders.Structure(
|
|
new XmlQualifiedName("Grids", ModelUri),
|
|
template.TypeId,
|
|
template.BinaryEncodingId,
|
|
template.XmlEncodingId,
|
|
CreateDefinition(),
|
|
fieldTypes);
|
|
|
|
ServiceMessageContext context = ServiceMessageContext.CreateEmpty(NUnitTelemetryContext.Create());
|
|
context.NamespaceUris = CreateNamespaceTable();
|
|
context.Factory.Builder
|
|
.AddEncodeableType(m_cellType)
|
|
.AddEncodeableType(structure)
|
|
.Commit();
|
|
return context;
|
|
}
|
|
|
|
private static object Encode(Codec codec, ServiceMessageContext context, IEncodeable value, ExpandedNodeId typeId)
|
|
{
|
|
switch (codec)
|
|
{
|
|
case Codec.Binary:
|
|
{
|
|
using var encoder = new BinaryEncoder(context);
|
|
encoder.WriteEncodeable("Grids", value, typeId);
|
|
return encoder.CloseAndReturnBuffer()!;
|
|
}
|
|
case Codec.JsonCompact:
|
|
case Codec.JsonVerbose:
|
|
{
|
|
using var encoder = new JsonEncoder(
|
|
context,
|
|
codec == Codec.JsonCompact ? JsonEncoderOptions.Compact : JsonEncoderOptions.Verbose);
|
|
encoder.WriteEncodeable("Grids", value, typeId);
|
|
return encoder.CloseAndReturnText();
|
|
}
|
|
default:
|
|
{
|
|
using var encoder = new XmlEncoder(context);
|
|
encoder.PushNamespace(ModelUri);
|
|
encoder.WriteEncodeable("Grids", value, typeId);
|
|
encoder.PopNamespace();
|
|
return encoder.CloseAndReturnText()!;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEncodeable Decode(
|
|
Codec codec,
|
|
ServiceMessageContext context,
|
|
object payload,
|
|
ExpandedNodeId typeId)
|
|
{
|
|
switch (codec)
|
|
{
|
|
case Codec.Binary:
|
|
{
|
|
using var decoder = new BinaryDecoder((byte[])payload, context);
|
|
return decoder.ReadEncodeable<IEncodeable>("Grids", typeId);
|
|
}
|
|
case Codec.JsonCompact:
|
|
case Codec.JsonVerbose:
|
|
{
|
|
using var decoder = new JsonDecoder((string)payload, context);
|
|
return decoder.ReadEncodeable<IEncodeable>("Grids", typeId);
|
|
}
|
|
case Codec.XmlParser:
|
|
{
|
|
using var parser = new XmlParser((string)payload, context);
|
|
parser.PushNamespace(ModelUri);
|
|
IEncodeable value = parser.ReadEncodeable<IEncodeable>("Grids", typeId);
|
|
parser.PopNamespace();
|
|
return value;
|
|
}
|
|
default:
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes((string)payload));
|
|
using var reader = XmlReader.Create(stream, CoreUtils.DefaultXmlReaderSettings());
|
|
using var decoder = new XmlDecoder(reader, context);
|
|
decoder.PushNamespace(ModelUri);
|
|
IEncodeable value = decoder.ReadEncodeable<IEncodeable>("Grids", typeId);
|
|
decoder.PopNamespace();
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private const string Model =
|
|
"""
|
|
<?xml version="1.0" encoding="utf-8" ?>
|
|
<opc:ModelDesign
|
|
xmlns:opc="http://opcfoundation.org/UA/ModelDesign.xsd"
|
|
xmlns:ua="http://opcfoundation.org/UA/"
|
|
xmlns="http://test.org/UA/MX/"
|
|
TargetNamespace="http://test.org/UA/MX/">
|
|
<opc:Namespaces>
|
|
<opc:Namespace Name="MX" Prefix="Test.MX">http://test.org/UA/MX/</opc:Namespace>
|
|
<opc:Namespace Name="OpcUa" Prefix="Opc.Ua" XmlNamespace="http://opcfoundation.org/UA/2008/02/Types.xsd">http://opcfoundation.org/UA/</opc:Namespace>
|
|
</opc:Namespaces>
|
|
<opc:DataType SymbolicName="Color" BaseType="ua:Enumeration">
|
|
<opc:Fields>
|
|
<opc:Field Name="Red" Identifier="0" />
|
|
<opc:Field Name="Green" Identifier="1" />
|
|
<opc:Field Name="Blue" Identifier="2" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
<opc:DataType SymbolicName="Flags" BaseType="ua:UInt32" IsOptionSet="true">
|
|
<opc:Fields>
|
|
<opc:Field Name="Low" BitMask="00000001" />
|
|
<opc:Field Name="High" BitMask="00000002" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
<opc:DataType SymbolicName="Cell" BaseType="ua:Structure">
|
|
<opc:Fields>
|
|
<opc:Field Name="V" DataType="ua:Int32" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
<opc:DataType SymbolicName="Grids" BaseType="ua:Structure">
|
|
<opc:Fields>
|
|
<opc:Field Name="Before" DataType="ua:Int32" />
|
|
<opc:Field Name="Doubles" DataType="ua:Double" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Strings" DataType="ua:String" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Colors" DataType="Color" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Flags" DataType="Flags" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Objects" DataType="ua:Structure" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Values" DataType="ua:BaseDataType" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Cells" DataType="Cell" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0" />
|
|
<opc:Field Name="Cube" DataType="ua:Int32" ValueRank="OneOrMoreDimensions" ArrayDimensions="0,0,0" />
|
|
<opc:Field Name="Row" DataType="ua:Double" ValueRank="OneOrMoreDimensions" ArrayDimensions="5" />
|
|
<opc:Field Name="After" DataType="ua:Int32" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
<opc:DataType SymbolicName="Unranked" BaseType="ua:Structure">
|
|
<opc:Fields>
|
|
<opc:Field Name="M" DataType="ua:Int32" ValueRank="OneOrMoreDimensions" />
|
|
<opc:Field Name="Tail" DataType="ua:Int32" />
|
|
</opc:Fields>
|
|
</opc:DataType>
|
|
</opc:ModelDesign>
|
|
""";
|
|
|
|
private static readonly string[] s_matrixFields =
|
|
["Doubles", "Strings", "Colors", "Flags", "Objects", "Values", "Cells", "Cube"];
|
|
private static readonly uint[] s_rowDimensions = [5];
|
|
private static readonly uint[] s_unknownTwoDimensions = [0, 0];
|
|
private static readonly int[] s_emptyRows = [0, 3];
|
|
private static readonly int[] s_emptyCube = [0, 0, 0];
|
|
private static readonly int[] s_emptyColumns = [2, 0];
|
|
private static readonly int[] s_twoByThree = [2, 3];
|
|
private static readonly int[] s_cubeDimensions = [2, 1, 2];
|
|
private static readonly int[] s_oneByTwo = [1, 2];
|
|
private static readonly double[] s_doubles = [1.5, 2.5, 3.5, 4.5, 5.5, 6.5];
|
|
private static readonly int[] s_cube = [1, 2, 3, 4];
|
|
private static readonly double[] s_row = [1, 2, 3, 4, 5];
|
|
private static readonly int[] s_colors = [0, 1, 2, 1];
|
|
private static readonly uint[] s_flags = [1, 2, 3, 0];
|
|
private Dictionary<string, string> m_generated;
|
|
private Assembly m_assembly;
|
|
private Type m_gridsType;
|
|
private Type m_cellType;
|
|
private Type m_colorType;
|
|
}
|
|
}
|