mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-16 21:02:51 +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>
1119 lines
41 KiB
C#
1119 lines
41 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/
|
|
* ======================================================================*/
|
|
|
|
// CA2000: test code; many disposables are ownership-transferred to test fixtures or short-lived,
|
|
// making CA2000 noisy without a real leak risk. Disabled file-level for the suite.
|
|
#pragma warning disable CA2000
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using System.Threading;
|
|
using System.Xml;
|
|
using NUnit.Framework;
|
|
using NUnit.Framework.Interfaces;
|
|
using Opc.Ua.Bindings;
|
|
using Opc.Ua.Test;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Core.TestFramework
|
|
{
|
|
/// <summary>
|
|
/// Supported memory stream types.
|
|
/// </summary>
|
|
public enum MemoryStreamType
|
|
{
|
|
MemoryStream,
|
|
ArraySegmentStream
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base class for the encoder tests.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Encoder")]
|
|
[SetCulture("en-us")]
|
|
public abstract class EncoderCommon
|
|
{
|
|
protected const int kArrayRepeats = 3;
|
|
protected const int kRandomStart = 4840;
|
|
protected const int kRandomRepeats = 100;
|
|
protected const int kMaxArrayLength = 1024 * 64;
|
|
protected const int kTestBlockSize = 0x1000;
|
|
protected const string kApplicationUri = "uri:localhost:opcfoundation.org:EncoderCommon";
|
|
|
|
private static readonly JsonSerializerOptions s_prettifyOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
AllowTrailingCommas = true
|
|
};
|
|
|
|
/// <summary>
|
|
/// Diagnostic output for the test that is currently running. Only written
|
|
/// out when the test does not pass.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The encoder tests dump the input, the encoded payload and the decoded
|
|
/// result of every case, which is what makes a failure diagnosable. There
|
|
/// are however thousands of cases, and the NUnit adapter forwards each
|
|
/// captured line to the test runner as an individual message over the
|
|
/// socket it shares with the test host. Emitting them unconditionally
|
|
/// produced roughly 500 MB of output per run of the encoder suite, which
|
|
/// throttled the run, made its published results a 164 MB artifact, and
|
|
/// eventually wedged that socket: the run then hung with no output at all
|
|
/// until the CI job timed out (issue #4213). Buffering the dumps and
|
|
/// writing them only when they are actually read - on a failure - keeps
|
|
/// the diagnostics without the flood.
|
|
/// </remarks>
|
|
protected TextWriter TestOutput { get; private set; } = null!;
|
|
|
|
protected RandomSource RandomSource { get; private set; } = null!;
|
|
protected DataGenerator DataGenerator { get; private set; } = null!;
|
|
protected IServiceMessageContext Context { get; private set; } = null!;
|
|
protected NamespaceTable NameSpaceUris { get; private set; } = null!;
|
|
protected StringTable ServerUris { get; private set; } = null!;
|
|
protected BufferManager BufferManager { get; private set; } = null!;
|
|
protected ITelemetryContext Telemetry { get; private set; } = null!;
|
|
|
|
[OneTimeSetUp]
|
|
protected void OneTimeSetUp()
|
|
{
|
|
Telemetry = NUnitTelemetryContext.Create();
|
|
Context = new ServiceMessageContext(Telemetry, EncodeableFactory.Create())
|
|
{
|
|
MaxArrayLength = kMaxArrayLength
|
|
};
|
|
NameSpaceUris = Context.NamespaceUris;
|
|
// namespace index 1 must be the ApplicationUri
|
|
NameSpaceUris.GetIndexOrAppend(kApplicationUri);
|
|
NameSpaceUris.GetIndexOrAppend(Namespaces.OpcUaGds);
|
|
ServerUris = new StringTable();
|
|
BufferManager = new BufferManager(nameof(EncoderCommon), kTestBlockSize, Telemetry);
|
|
}
|
|
|
|
[OneTimeTearDown]
|
|
protected void OneTimeTearDown()
|
|
{
|
|
}
|
|
|
|
[SetUp]
|
|
protected void SetUp()
|
|
{
|
|
// One writer per fixture over the reused buffer; the buffer, not the
|
|
// writer, carries the per-test state.
|
|
TestOutput ??= new StringWriter(m_testOutput, CultureInfo.CurrentCulture);
|
|
m_testOutput.Clear();
|
|
// ensure tests are reproducible, reset for every test
|
|
RandomSource = new RandomSource(kRandomStart);
|
|
DataGenerator = new DataGenerator(RandomSource, Telemetry);
|
|
}
|
|
|
|
[TearDown]
|
|
protected void TearDown()
|
|
{
|
|
FlushTestOutput();
|
|
// ensure after every test that the Null NodeId was not modified
|
|
Assert.That(NodeId.Null.IsNull, Is.True);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hand the buffered diagnostics to NUnit when the test did not pass, and
|
|
/// drop them otherwise. See <see cref="TestOutput"/> for why they are not
|
|
/// written as they are produced.
|
|
/// </summary>
|
|
private void FlushTestOutput()
|
|
{
|
|
if (m_testOutput.Length != 0 &&
|
|
TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Passed)
|
|
{
|
|
TestContext.Out.Write(m_testOutput.ToString());
|
|
}
|
|
m_testOutput.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure repeated tests get different seed.
|
|
/// </summary>
|
|
protected void SetRepeatedRandomSeed()
|
|
{
|
|
int randomSeed = TestContext.CurrentContext.CurrentRepeatCount + kRandomStart;
|
|
RandomSource = new RandomSource(randomSeed);
|
|
DataGenerator = new DataGenerator(RandomSource, Telemetry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure tests are reproducible with same seed.
|
|
/// </summary>
|
|
protected void SetRandomSeed(int randomSeed)
|
|
{
|
|
RandomSource = new RandomSource(randomSeed + kRandomStart);
|
|
DataGenerator = new DataGenerator(RandomSource, Telemetry);
|
|
}
|
|
|
|
[DatapointSource]
|
|
public static readonly BuiltInType[] BuiltInTypes =
|
|
#if NET8_0_OR_GREATER
|
|
[
|
|
.. Enum.GetValues<BuiltInType>()
|
|
#else
|
|
[
|
|
.. Enum.GetValues(typeof(BuiltInType))
|
|
.Cast<BuiltInType>()
|
|
#endif
|
|
.Where(b =>
|
|
b
|
|
is not BuiltInType.Variant
|
|
and not BuiltInType.DiagnosticInfo
|
|
and not BuiltInType.DataValue
|
|
and (< BuiltInType.Number or > BuiltInType.UInteger))
|
|
];
|
|
|
|
[DatapointSource]
|
|
public static readonly EncodingType[] EncoderTypes =
|
|
#if NET8_0_OR_GREATER
|
|
Enum.GetValues<EncodingType>();
|
|
#else
|
|
(EncodingType[])Enum.GetValues(typeof(EncodingType));
|
|
#endif
|
|
|
|
public static readonly EncodingTypeGroup[] EncodingTypesJson =
|
|
[
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact),
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose)
|
|
];
|
|
|
|
public static readonly EncodingTypeGroup[] EncodingTypesJsonVerbose =
|
|
[
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose)
|
|
];
|
|
|
|
public static readonly EncodingTypeGroup[] EncodingTypesJsonBinaryXmlAndJsonCompact =
|
|
[
|
|
new EncodingTypeGroup(EncodingType.Binary),
|
|
new EncodingTypeGroup(EncodingType.Xml, useXmlParser: false),
|
|
new EncodingTypeGroup(EncodingType.Xml, useXmlParser: true),
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact)
|
|
];
|
|
|
|
public static readonly EncodingTypeGroup[] EncodingTypesAll =
|
|
[
|
|
new EncodingTypeGroup(EncodingType.Binary),
|
|
new EncodingTypeGroup(EncodingType.Xml, useXmlParser: false),
|
|
new EncodingTypeGroup(EncodingType.Xml, useXmlParser: true),
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Compact),
|
|
new EncodingTypeGroup(EncodingType.Json, JsonEncodingType.Verbose)
|
|
];
|
|
|
|
/// <summary>
|
|
/// Encode data value and return encoded string.
|
|
/// </summary>
|
|
protected string EncodeDataValue(
|
|
EncodingType encoderType,
|
|
BuiltInType builtInType,
|
|
MemoryStreamType memoryStreamType,
|
|
Variant data,
|
|
JsonEncodingType encoding)
|
|
{
|
|
string encodeInfo = $"Encoder: {encoderType} Type:{builtInType} Encoding:{encoding}";
|
|
TestOutput.WriteLine(encodeInfo);
|
|
TestOutput.WriteLine(data);
|
|
DataValue expected = CreateDataValue(data);
|
|
TestOutput.WriteLine("Expected:");
|
|
TestOutput.WriteLine(expected);
|
|
Assert.That(expected.IsNull, Is.False, "Expected DataValue is Null, " + encodeInfo);
|
|
using MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType);
|
|
using (IEncoder encoder = CreateEncoder(
|
|
encoderType,
|
|
Context,
|
|
encoderStream,
|
|
typeof(DataValue),
|
|
encoding))
|
|
{
|
|
encoder.WriteDataValue("DataValue", expected);
|
|
}
|
|
byte[] buffer = encoderStream.ToArray();
|
|
return Encoding.UTF8.GetString(buffer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode and decode a DataValue,
|
|
/// validate the result against the input data.
|
|
/// </summary>
|
|
protected void EncodeDecodeDataValue(
|
|
EncodingType encoderType,
|
|
JsonEncodingType jsonEncodingType,
|
|
bool useXmlParser,
|
|
BuiltInType builtInType,
|
|
MemoryStreamType memoryStreamType,
|
|
Variant data)
|
|
{
|
|
string encodeInfo = $"Encoder: {encoderType} Type:{builtInType}";
|
|
TestOutput.WriteLine(encodeInfo);
|
|
TestOutput.WriteLine(data);
|
|
DataValue expected = CreateDataValue(data);
|
|
Assert.That(expected.IsNull, Is.False, "Expected DataValue is Null, " + encodeInfo);
|
|
|
|
string? formatted = null;
|
|
DataValue result = default;
|
|
try
|
|
{
|
|
byte[] buffer;
|
|
using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType))
|
|
{
|
|
using (
|
|
IEncoder encoder = CreateEncoder(
|
|
encoderType,
|
|
Context,
|
|
encoderStream,
|
|
typeof(DataValue),
|
|
jsonEncodingType))
|
|
{
|
|
encoder.WriteDataValue("DataValue", expected);
|
|
}
|
|
buffer = encoderStream.ToArray();
|
|
}
|
|
|
|
switch (encoderType)
|
|
{
|
|
case EncodingType.Json:
|
|
formatted = PrettifyAndValidateJson(buffer);
|
|
break;
|
|
case EncodingType.Xml:
|
|
formatted = PrettifyAndValidateXml(buffer);
|
|
break;
|
|
}
|
|
|
|
using (var decoderStream = new MemoryStream(buffer))
|
|
using (IDecoder decoder = CreateDecoder(
|
|
encoderType,
|
|
useXmlParser,
|
|
Context,
|
|
decoderStream,
|
|
typeof(DataValue)))
|
|
{
|
|
result = decoder.ReadDataValue("DataValue");
|
|
}
|
|
|
|
Assert.That(result.IsNull, Is.False, "Resulting DataValue is Null, " + encodeInfo);
|
|
Assert.That(result, Is.EqualTo(expected), encodeInfo);
|
|
Assert.That(
|
|
Utils.IsEqual(expected, result),
|
|
Is.True,
|
|
"Opc.Ua.Utils.IsEqual failed to compare expected and result. " + encodeInfo);
|
|
}
|
|
catch
|
|
{
|
|
TestOutput.WriteLine("Expected:");
|
|
TestOutput.WriteLine(expected);
|
|
if (formatted != null)
|
|
{
|
|
TestOutput.WriteLine("Encoded:");
|
|
TestOutput.WriteLine(formatted);
|
|
}
|
|
|
|
TestOutput.WriteLine("Result:");
|
|
if (!result.IsNull)
|
|
{
|
|
TestOutput.WriteLine(result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode and decode Variant, validate result.
|
|
/// </summary>
|
|
protected void EncodeDecode(
|
|
EncodingType encoderType,
|
|
JsonEncodingType jsonEncodingType,
|
|
bool useXmlParser,
|
|
BuiltInType builtInType,
|
|
MemoryStreamType memoryStreamType,
|
|
Variant expected)
|
|
{
|
|
string? formatted = null;
|
|
Variant result = default;
|
|
try
|
|
{
|
|
string encodeInfo = $"Encoder: {encoderType} Type:{builtInType}";
|
|
IBuiltInType type = TypeInfo.GetSystemType(builtInType);
|
|
TestOutput.WriteLine(encodeInfo);
|
|
|
|
byte[] buffer;
|
|
using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType))
|
|
{
|
|
using (
|
|
IEncoder encoder = CreateEncoder(
|
|
encoderType,
|
|
Context,
|
|
encoderStream,
|
|
(type?.Type)!,
|
|
jsonEncodingType))
|
|
{
|
|
encoder.WriteVariantValue(builtInType.ToString(), expected);
|
|
}
|
|
buffer = encoderStream.ToArray();
|
|
}
|
|
|
|
switch (encoderType)
|
|
{
|
|
case EncodingType.Json:
|
|
formatted = PrettifyAndValidateJson(buffer);
|
|
break;
|
|
case EncodingType.Xml:
|
|
formatted = PrettifyAndValidateXml(buffer);
|
|
break;
|
|
default:
|
|
formatted = Encoding.UTF8.GetString(buffer);
|
|
break;
|
|
}
|
|
|
|
using (var decoderStream = new MemoryStream(buffer))
|
|
using (IDecoder decoder = CreateDecoder(
|
|
encoderType,
|
|
useXmlParser,
|
|
Context,
|
|
decoderStream,
|
|
(type?.Type)!))
|
|
{
|
|
result = decoder.ReadVariantValue(
|
|
builtInType.ToString(),
|
|
expected.TypeInfo);
|
|
}
|
|
|
|
Assert.That(result, Is.EqualTo(expected), encodeInfo);
|
|
}
|
|
catch
|
|
{
|
|
// only print infos if test fails, to reduce log output
|
|
TestOutput.WriteLine("Expected:");
|
|
TestOutput.WriteLine(expected);
|
|
TestOutput.WriteLine("Result:");
|
|
TestOutput.WriteLine(result);
|
|
if (formatted != null)
|
|
{
|
|
TestOutput.WriteLine("Encoded:");
|
|
TestOutput.WriteLine(formatted);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encode Variant as JSON and validate against expected JSON string.
|
|
/// </summary>
|
|
protected void EncodeJsonVerifyResult(
|
|
BuiltInType builtInType,
|
|
MemoryStreamType memoryStreamType,
|
|
Variant data,
|
|
JsonEncodingType jsonEncoding,
|
|
string expected)
|
|
{
|
|
string? result = null;
|
|
string? formattedResult = null;
|
|
try
|
|
{
|
|
string encodeInfo = $"Encoder: Json Type:{builtInType} Encoding: {jsonEncoding}";
|
|
TestOutput.WriteLine(encodeInfo);
|
|
if (!string.IsNullOrEmpty(expected))
|
|
{
|
|
expected = $"{{\"{builtInType}\":" + expected + "}";
|
|
}
|
|
else
|
|
{
|
|
expected = "{}";
|
|
}
|
|
|
|
byte[] buffer;
|
|
using (MemoryStream encoderStream = CreateEncoderMemoryStream(memoryStreamType))
|
|
{
|
|
using (
|
|
IEncoder encoder = CreateEncoder(
|
|
EncodingType.Json,
|
|
Context,
|
|
encoderStream,
|
|
typeof(DataValue),
|
|
jsonEncoding))
|
|
{
|
|
if (builtInType == BuiltInType.Variant)
|
|
{
|
|
encoder.WriteVariant(builtInType.ToString(), data);
|
|
}
|
|
else
|
|
{
|
|
encoder.WriteVariantValue(builtInType.ToString(), data);
|
|
}
|
|
}
|
|
buffer = encoderStream.ToArray();
|
|
}
|
|
|
|
TestOutput.WriteLine("Result:");
|
|
result = Encoding.UTF8.GetString(buffer);
|
|
formattedResult = PrettifyAndValidateJson(result);
|
|
var resultParsed = JsonNode.Parse(result,
|
|
documentOptions: new JsonDocumentOptions { AllowTrailingCommas = true });
|
|
var expectedParsed = JsonNode.Parse(expected,
|
|
documentOptions: new JsonDocumentOptions { AllowTrailingCommas = true });
|
|
bool areEqual = JsonNode.DeepEquals(expectedParsed, resultParsed);
|
|
Assert.That(areEqual, Is.True, encodeInfo);
|
|
}
|
|
catch
|
|
{
|
|
TestOutput.WriteLine("Data:");
|
|
TestOutput.WriteLine(data);
|
|
TestOutput.WriteLine("Expected:");
|
|
string formattedExpected = PrettifyAndValidateJson(expected);
|
|
TestOutput.WriteLine(formattedExpected);
|
|
TestOutput.WriteLine("Result:");
|
|
if (!string.IsNullOrEmpty(formattedResult))
|
|
{
|
|
TestOutput.WriteLine(formattedResult);
|
|
}
|
|
else
|
|
{
|
|
TestOutput.WriteLine(result);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format and validate a XML document string.
|
|
/// </summary>
|
|
protected string PrettifyAndValidateXml(byte[] xml, bool outputFormatted = false)
|
|
{
|
|
try
|
|
{
|
|
using var reader = new MemoryStream(xml);
|
|
using var xmlReader = XmlReader.Create(reader, Utils.DefaultXmlReaderSettings());
|
|
var document = new XmlDocument();
|
|
document.Load(xmlReader);
|
|
|
|
var settings = new XmlWriterSettings
|
|
{
|
|
OmitXmlDeclaration = true,
|
|
Indent = true,
|
|
NewLineOnAttributes = true
|
|
};
|
|
|
|
var stringBuilder = new StringBuilder();
|
|
using (var xmlWriter = XmlWriter.Create(stringBuilder, settings))
|
|
{
|
|
document.Save(xmlWriter);
|
|
}
|
|
string formattedXml = stringBuilder.ToString();
|
|
if (outputFormatted)
|
|
{
|
|
TestContext.Out.WriteLine(formattedXml);
|
|
}
|
|
return formattedXml;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TestContext.Out.WriteLine(xml);
|
|
Assert.Fail("Invalid xml data: " + ex.Message);
|
|
}
|
|
return Encoding.UTF8.GetString(xml);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format binary data
|
|
/// </summary>
|
|
public static string PrettifyAndValidateBinary(byte[] buffer, bool outputFormatted = false)
|
|
{
|
|
return CoreUtils.ToHexString(buffer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format and validate a JSON string.
|
|
/// </summary>
|
|
public static string PrettifyAndValidateJson(byte[] json, bool outputFormatted = false)
|
|
{
|
|
return PrettifyAndValidateJson(Encoding.UTF8.GetString(json), outputFormatted);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format and validate a JSON string.
|
|
/// </summary>
|
|
public static string PrettifyAndValidateJson(string json, bool outputFormatted = false)
|
|
{
|
|
try
|
|
{
|
|
var jsonDocument = JsonDocument.Parse(json,
|
|
new JsonDocumentOptions { AllowTrailingCommas = true });
|
|
string formattedJson = JsonSerializer.Serialize(jsonDocument, s_prettifyOptions);
|
|
if (outputFormatted)
|
|
{
|
|
TestContext.Out.WriteLine(formattedJson);
|
|
}
|
|
return formattedJson;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TestContext.Out.WriteLine(json);
|
|
Assert.Fail("Invalid json data: " + ex.Message);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns various implementations of a memory stream.
|
|
/// </summary>
|
|
/// <returns>A MemoryStream</returns>
|
|
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|
protected MemoryStream CreateEncoderMemoryStream(MemoryStreamType memoryStreamType)
|
|
{
|
|
switch (memoryStreamType)
|
|
{
|
|
case MemoryStreamType.MemoryStream:
|
|
return new MemoryStream(kTestBlockSize);
|
|
case MemoryStreamType.ArraySegmentStream:
|
|
return new ArraySegmentStream(BufferManager);
|
|
default:
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(memoryStreamType),
|
|
memoryStreamType,
|
|
"Invalid MemoryStreamType specified.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encoder factory for all encoding types.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|
protected IEncoder CreateEncoder(
|
|
EncodingType encoderType,
|
|
IServiceMessageContext context,
|
|
Stream stream,
|
|
Type systemType,
|
|
JsonEncodingType jsonEncoding = JsonEncodingType.Verbose)
|
|
{
|
|
switch (encoderType)
|
|
{
|
|
case EncodingType.Binary:
|
|
return new BinaryEncoder(stream, context, true);
|
|
case EncodingType.Xml:
|
|
var xmlWriter = XmlWriter.Create(stream, Utils.DefaultXmlWriterSettings());
|
|
return new XmlEncoder(systemType, xmlWriter, context);
|
|
case EncodingType.Json:
|
|
return new JsonEncoder(
|
|
stream,
|
|
context,
|
|
jsonEncoding == JsonEncodingType.Verbose ? JsonEncoderOptions.Verbose : JsonEncoderOptions.Compact);
|
|
default:
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(encoderType),
|
|
encoderType,
|
|
"Invalid EncoderType specified.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decoder factory for all decoding types.
|
|
/// </summary>
|
|
protected IDecoder CreateDecoder(
|
|
EncodingType decoderType,
|
|
bool useXmlParser,
|
|
IServiceMessageContext context,
|
|
Stream stream,
|
|
Type systemType)
|
|
{
|
|
switch (decoderType)
|
|
{
|
|
case EncodingType.Binary:
|
|
return new BinaryDecoder(stream, context);
|
|
case EncodingType.Xml when useXmlParser:
|
|
return new XmlParser(systemType, stream, context);
|
|
case EncodingType.Xml:
|
|
var xmlReader = XmlReader.Create(stream, Utils.DefaultXmlReaderSettings());
|
|
return new XmlDecoder(systemType, xmlReader, context);
|
|
case EncodingType.Json:
|
|
return new JsonDecoder(stream, context);
|
|
default:
|
|
return null!;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wrap Variant in a DataValue.
|
|
/// </summary>
|
|
protected DataValue CreateDataValue(Variant variant)
|
|
{
|
|
StatusCode statusCode = DataGenerator.GetRandomStatusCode();
|
|
DateTimeUtc sourceTimeStamp = DataGenerator.GetRandomDateTime();
|
|
return new DataValue(variant, statusCode, sourceTimeStamp, DateTime.UtcNow);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to add escaped quotes to a string.
|
|
/// </summary>
|
|
protected static string Quotes(string json)
|
|
{
|
|
return "\"" + json + "\"";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if system Type is IEncodeable.
|
|
/// </summary>
|
|
protected static bool IsEncodeableType(Type systemType)
|
|
{
|
|
if (systemType == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
System.Reflection.TypeInfo systemTypeInfo = systemType.GetTypeInfo();
|
|
if (systemTypeInfo.IsAbstract ||
|
|
!typeof(IEncodeable).GetTypeInfo().IsAssignableFrom(systemTypeInfo) ||
|
|
typeof(Encoders.Structure).IsAssignableFrom(systemType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Activator.CreateInstance(systemType) is IEncodeable;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the number of elements from a dimension array.
|
|
/// </summary>
|
|
protected static int ElementsFromDimension(int[] dimensions)
|
|
{
|
|
int elements = 1;
|
|
for (int i = 0; i < dimensions.Length; i++)
|
|
{
|
|
if (dimensions[i] != 0)
|
|
{
|
|
elements *= dimensions[i];
|
|
}
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets random array dimensions between 2 and 10.
|
|
/// Number of total elements is limited by <see cref="kMaxArrayLength"/>
|
|
/// </summary>
|
|
protected void SetMatrixDimensions(int[] dimensions)
|
|
{
|
|
int totalElements = 1;
|
|
for (int i = 0; i < dimensions.Length; i++)
|
|
{
|
|
dimensions[i] = RandomSource.NextInt32(8) + 2;
|
|
totalElements *= dimensions[i];
|
|
}
|
|
while (totalElements > kMaxArrayLength)
|
|
{
|
|
int random = RandomSource.NextInt32(dimensions.Length - 1);
|
|
if (dimensions[random] > 1)
|
|
{
|
|
dimensions[random]--;
|
|
}
|
|
totalElements = 1;
|
|
for (int i = 0; i < dimensions.Length; i++)
|
|
{
|
|
totalElements *= dimensions[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly StringBuilder m_testOutput = new();
|
|
|
|
protected enum TestEnumType
|
|
{
|
|
[EnumMember(Value = "One_1")]
|
|
One = 1,
|
|
|
|
[EnumMember(Value = "Two_2")]
|
|
Two = 2,
|
|
|
|
[EnumMember(Value = "Three_3")]
|
|
Three = 3,
|
|
|
|
[EnumMember(Value = "Ten_10")]
|
|
Ten = 10,
|
|
|
|
[EnumMember(Value = "Hundred_100")]
|
|
Hundred = 100
|
|
}
|
|
|
|
protected class FooBarEncodeable : IEncodeable, IDisposable
|
|
{
|
|
private static int s_count;
|
|
|
|
public FooBarEncodeable()
|
|
{
|
|
m_resetCounter = true;
|
|
Count = Interlocked.Increment(ref s_count);
|
|
Foo = $"bar_{Count}";
|
|
FieldName = nameof(Foo);
|
|
}
|
|
|
|
public FooBarEncodeable(int count)
|
|
{
|
|
Count = count;
|
|
Foo = $"bar_{Count}";
|
|
FieldName = nameof(Foo);
|
|
}
|
|
|
|
public FooBarEncodeable(string foo)
|
|
{
|
|
Foo = foo;
|
|
FieldName = nameof(Foo);
|
|
}
|
|
|
|
public FooBarEncodeable(string fieldname, string foo)
|
|
{
|
|
Foo = foo;
|
|
FieldName = fieldname;
|
|
}
|
|
|
|
public string Foo { get; set; }
|
|
public string FieldName { get; set; }
|
|
public int Count { get; set; }
|
|
|
|
public ExpandedNodeId TypeId { get; }
|
|
public ExpandedNodeId BinaryEncodingId { get; }
|
|
public ExpandedNodeId XmlEncodingId { get; }
|
|
|
|
public void Encode(IEncoder encoder)
|
|
{
|
|
encoder.PushNamespace(kApplicationUri);
|
|
encoder.WriteString(FieldName, Foo);
|
|
encoder.PopNamespace();
|
|
}
|
|
|
|
public void Decode(IDecoder decoder)
|
|
{
|
|
decoder.PushNamespace(kApplicationUri);
|
|
Foo = decoder.ReadString(FieldName)!;
|
|
decoder.PopNamespace();
|
|
}
|
|
|
|
public bool IsEqual(IEncodeable? encodeable)
|
|
{
|
|
if (encodeable is FooBarEncodeable de)
|
|
{
|
|
return Foo == de.Foo;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing && m_resetCounter)
|
|
{
|
|
s_count = 0;
|
|
}
|
|
// free unmanaged resources
|
|
}
|
|
|
|
public virtual object Clone()
|
|
{
|
|
return MemberwiseClone();
|
|
}
|
|
|
|
public new object MemberwiseClone()
|
|
{
|
|
return new FooBarEncodeable(FieldName, Foo) { Count = Count };
|
|
}
|
|
|
|
private readonly bool m_resetCounter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A simple dynamic encodeable that can handle arbitrary fields of type string
|
|
/// </summary>
|
|
protected class DynamicEncodeable :
|
|
IEncodeable,
|
|
IDisposable,
|
|
IDynamicComplexTypeInstance
|
|
{
|
|
private static int s_count;
|
|
|
|
public DynamicEncodeable()
|
|
{
|
|
}
|
|
|
|
public DynamicEncodeable(
|
|
string xmlName,
|
|
string xmlNamespace,
|
|
ExpandedNodeId typeId,
|
|
ExpandedNodeId binaryEncodingId,
|
|
ExpandedNodeId xmlEncodingId)
|
|
: this(
|
|
xmlName,
|
|
xmlNamespace,
|
|
typeId,
|
|
binaryEncodingId,
|
|
xmlEncodingId,
|
|
(Dictionary<string, (int, string)>)null!)
|
|
{
|
|
m_resetCounter = true;
|
|
Count = Interlocked.Increment(ref s_count);
|
|
|
|
m_fields = new Dictionary<string, (int, string)> { { "Foo", (1, $"bar_{Count}") } };
|
|
}
|
|
|
|
public DynamicEncodeable(
|
|
string xmlName,
|
|
string xmlNamespace,
|
|
ExpandedNodeId typeId,
|
|
ExpandedNodeId binaryEncodingId,
|
|
ExpandedNodeId xmlEncodingId,
|
|
int count)
|
|
: this(
|
|
xmlName,
|
|
xmlNamespace,
|
|
typeId,
|
|
binaryEncodingId,
|
|
xmlEncodingId,
|
|
new Dictionary<string, (int, string)> { { "Foo", (1, $"bar_{count}") } })
|
|
{
|
|
Count = count;
|
|
}
|
|
|
|
public DynamicEncodeable(
|
|
string xmlName,
|
|
string xmlNamespace,
|
|
ExpandedNodeId typeId,
|
|
ExpandedNodeId binaryEncodingId,
|
|
ExpandedNodeId xmlEncodingId,
|
|
string foo)
|
|
: this(
|
|
xmlName,
|
|
xmlNamespace,
|
|
typeId,
|
|
binaryEncodingId,
|
|
xmlEncodingId,
|
|
new Dictionary<string, (int, string)> { { "Foo", (1, foo) } })
|
|
{
|
|
}
|
|
|
|
public DynamicEncodeable(
|
|
string xmlName,
|
|
string xmlNamespace,
|
|
ExpandedNodeId typeId,
|
|
ExpandedNodeId binaryEncodingId,
|
|
ExpandedNodeId xmlEncodingId,
|
|
Dictionary<string, (int, string)> fields)
|
|
{
|
|
m_xmlName = xmlName;
|
|
m_xmlNamespace = xmlNamespace;
|
|
TypeId = typeId;
|
|
BinaryEncodingId = binaryEncodingId;
|
|
XmlEncodingId = xmlEncodingId;
|
|
|
|
m_fields = fields;
|
|
}
|
|
|
|
public int Count { get; set; }
|
|
|
|
public ExpandedNodeId TypeId { get; set; }
|
|
public ExpandedNodeId BinaryEncodingId { get; set; }
|
|
public ExpandedNodeId XmlEncodingId { get; set; }
|
|
|
|
public void Encode(IEncoder encoder)
|
|
{
|
|
InitializeFromFactory((encoder.Context?.Factory)!);
|
|
encoder.PushNamespace(m_xmlNamespace);
|
|
foreach (
|
|
KeyValuePair<string, (int FieldOrder, string Value)> field in m_fields
|
|
.OrderBy(kv => kv.Value.FieldOrder)
|
|
.ToList())
|
|
{
|
|
encoder.WriteString(field.Key, field.Value.Value);
|
|
}
|
|
encoder.PopNamespace();
|
|
}
|
|
|
|
public void Decode(IDecoder decoder)
|
|
{
|
|
InitializeFromFactory((decoder.Context?.Factory)!);
|
|
decoder.PushNamespace(m_xmlNamespace);
|
|
foreach (
|
|
KeyValuePair<string, (int FieldOrder, string Value)> fieldKV in m_fields
|
|
.OrderBy(kv => kv.Value.FieldOrder)
|
|
.ToList())
|
|
{
|
|
m_fields[fieldKV.Key] = (fieldKV.Value.FieldOrder, decoder.ReadString(
|
|
fieldKV.Key)!);
|
|
}
|
|
decoder.PopNamespace();
|
|
}
|
|
|
|
private void InitializeFromFactory(IEncodeableFactory factory)
|
|
{
|
|
if (m_fields == null)
|
|
{
|
|
// When the dynamic encodeable is instantiated by a encoder/decoder,
|
|
// it needs to find it's type information
|
|
|
|
// Obtain a previously registered instance from the Factory
|
|
// Other systems will want to put just type information into the factory,
|
|
// or have other means of finding type information given an encoding id
|
|
DynamicEncodeable encodeable = (factory is DynamicEncodeableFactory df
|
|
? df.GetDynamicEncodeableForEncoding(TypeId)
|
|
: null)!;
|
|
// Read the type information
|
|
TypeId = encodeable?.TypeId ?? default;
|
|
XmlEncodingId = encodeable?.XmlEncodingId ?? default;
|
|
BinaryEncodingId = encodeable?.BinaryEncodingId ?? default;
|
|
Count = encodeable?.Count ?? 0;
|
|
m_fields = (encodeable?.m_fields.ToDictionary(
|
|
kv => kv.Key,
|
|
kv => (kv.Value.FieldOrder, (string)null!)))!;
|
|
m_xmlName = (encodeable?.m_xmlName)!;
|
|
m_xmlNamespace = (encodeable?.m_xmlNamespace)!;
|
|
}
|
|
}
|
|
|
|
public bool IsEqual(IEncodeable? encodeable)
|
|
{
|
|
if (encodeable is DynamicEncodeable de)
|
|
{
|
|
return m_fields.OrderBy(kv => kv.Key)
|
|
.SequenceEqual(de.m_fields.OrderBy(kv => kv.Key));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing && m_resetCounter)
|
|
{
|
|
s_count = 0;
|
|
}
|
|
}
|
|
|
|
public virtual object Clone()
|
|
{
|
|
return MemberwiseClone();
|
|
}
|
|
|
|
public new object MemberwiseClone()
|
|
{
|
|
return new DynamicEncodeable(
|
|
m_xmlName,
|
|
m_xmlNamespace,
|
|
TypeId,
|
|
BinaryEncodingId,
|
|
XmlEncodingId,
|
|
m_fields.ToDictionary(kv => kv.Key, kv => kv.Value))
|
|
{
|
|
Count = Count
|
|
};
|
|
}
|
|
|
|
public XmlQualifiedName GetXmlName(IServiceMessageContext context)
|
|
{
|
|
InitializeFromFactory((context?.Factory)!);
|
|
return new XmlQualifiedName(m_xmlName, m_xmlNamespace);
|
|
}
|
|
|
|
private Dictionary<string, (int FieldOrder, string Value)> m_fields = null!;
|
|
private string m_xmlName = null!;
|
|
private string m_xmlNamespace = null!;
|
|
private readonly bool m_resetCounter;
|
|
}
|
|
|
|
protected class DynamicEncodeableFactory : IEncodeableFactory
|
|
{
|
|
public DynamicEncodeableFactory(IEncodeableFactory factory)
|
|
{
|
|
m_inner = factory;
|
|
}
|
|
|
|
public IEnumerable<ExpandedNodeId> KnownTypeIds => m_inner.KnownTypeIds;
|
|
|
|
public IEncodeableFactoryBuilder Builder => m_inner.Builder;
|
|
|
|
public DynamicEncodeable GetDynamicEncodeableForEncoding(ExpandedNodeId typeId)
|
|
{
|
|
if (!typeId.IsNull &&
|
|
m_dynamicEncodeables.TryGetValue(
|
|
typeId,
|
|
out DynamicEncodeable? dynamicEncodeable))
|
|
{
|
|
return dynamicEncodeable;
|
|
}
|
|
return null!;
|
|
}
|
|
|
|
public void AddDynamicEncodeable(DynamicEncodeable encodeable)
|
|
{
|
|
m_dynamicEncodeables[encodeable.XmlEncodingId] = encodeable;
|
|
m_dynamicEncodeables[encodeable.BinaryEncodingId] = encodeable;
|
|
m_dynamicEncodeables[encodeable.TypeId] = encodeable;
|
|
m_inner.AddEncodeableType(encodeable.XmlEncodingId, typeof(DynamicEncodeable));
|
|
m_inner.AddEncodeableType(encodeable.BinaryEncodingId, typeof(DynamicEncodeable));
|
|
m_inner.AddEncodeableType(encodeable.TypeId, typeof(DynamicEncodeable));
|
|
}
|
|
|
|
public bool TryGetEncodeableType(ExpandedNodeId typeId, [NotNullWhen(true)] out IEncodeableType encodeableType)
|
|
{
|
|
return m_inner.TryGetEncodeableType(typeId, out encodeableType!);
|
|
}
|
|
|
|
public bool TryGetEnumeratedType(ExpandedNodeId typeId, [NotNullWhen(true)] out IEnumeratedType enumeratedType)
|
|
{
|
|
return m_inner.TryGetEnumeratedType(typeId, out enumeratedType!);
|
|
}
|
|
|
|
public bool TryGetType(XmlQualifiedName xmlName, [NotNullWhen(true)] out IType type)
|
|
{
|
|
return m_inner.TryGetType(xmlName, out type!);
|
|
}
|
|
|
|
private readonly IEncodeableFactory m_inner;
|
|
private readonly Dictionary<ExpandedNodeId, DynamicEncodeable> m_dynamicEncodeables = [];
|
|
}
|
|
}
|
|
}
|