Files
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# 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>
2026-10-08 08:48:13 +02:00

1917 lines
63 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.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using BenchmarkDotNet.Attributes;
using NUnit.Framework;
using Opc.Ua.Bindings;
using Opc.Ua.Core.TestFramework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Encoders.Tests
{
/// <summary>
/// Tests for the Json encoder and decoder class.
/// </summary>
[TestFixture]
[Category("JsonEncoder")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
[MemoryDiagnoser]
[DisassemblyDiagnoser]
public class JsonEncoderTests : EncoderCommon
{
private static readonly TestEnumType[] s_testEnumArray =
[
TestEnumType.One,
TestEnumType.Two,
TestEnumType.Hundred
];
private static readonly int[] s_testInt32Array = [2, 3, 10];
private static readonly ExtensionObject s_testEncodeable = new(new FooBarEncodeable(999));
/// <summary>
/// Constants used by test data set.
/// </summary>
private const ushort kDemoServerIndex = 3;
private const string kDemoServer = "http://www.opcfoundation.org/DemoServer/";
private const string kDemoServer2 = "http://www.opcfoundation.org/DemoServer2/";
private const string kNodeIdString = "theNode";
private const string kQualifiedName = "theName";
private const string kLocalizedText = "theText";
private const string kLocale = "en-us";
private const int kNodeIdInt = 2345;
private const long kInt64Value = -123456789123456;
private const ulong kUInt64Value = 123456789123456;
private static readonly Uuid s_nodeIdGuid = new(new Guid("AABA0CFA-674F-40C7-B7FA-339D8EECB61D"));
private static readonly ByteString s_byteString = ByteString.From([1, 2, 3, 4, 5, 6, 7, 8, 9]);
private static readonly string s_byteString64 = s_byteString.ToBase64();
/// <summary>
/// An array of spec compliant Json encoding test data sets which
/// shall be followed by the JSON encoder accordingly.
/// </summary>
[DatapointSource]
public static readonly JsonValidationData[] Data = new JsonValidationDataCollection
{
{
BuiltInType.Boolean,
true,
"true",
null!
},
{
BuiltInType.Boolean,
false,
null!,
"false"
},
{
BuiltInType.Byte,
(byte)0,
null!,
"0"
},
{
BuiltInType.Byte,
(byte)88,
"88",
null!
},
{
BuiltInType.Byte,
(byte)188,
"188",
null!
},
{
BuiltInType.Byte,
byte.MinValue,
null!,
byte.MinValue.ToString(CultureInfo.InvariantCulture)
},
{
BuiltInType.Byte,
byte.MaxValue,
byte.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.SByte,
(sbyte)0,
null!,
"0"
},
{
BuiltInType.SByte,
(sbyte)-77,
"-77",
null!
},
{
BuiltInType.SByte,
(sbyte)77,
"77",
null!
},
{
BuiltInType.SByte,
sbyte.MaxValue,
sbyte.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.SByte,
sbyte.MinValue,
sbyte.MinValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.UInt16,
(ushort)0,
null!,
"0"
},
{
BuiltInType.UInt16,
(ushort)12345,
"12345",
null!
},
{
BuiltInType.UInt16,
(ushort)44444,
"44444",
null!
},
{
BuiltInType.UInt16,
ushort.MinValue,
null!,
ushort.MinValue.ToString(CultureInfo.InvariantCulture)
},
{
BuiltInType.UInt16,
ushort.MaxValue,
ushort.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Int16,
(short)0,
null!,
"0"
},
{
BuiltInType.Int16,
(short)-12345,
"-12345",
null!
},
{
BuiltInType.Int16,
(short)12345,
"12345",
null!
},
{
BuiltInType.Int16,
short.MaxValue,
short.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Int16,
short.MinValue,
short.MinValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.UInt32,
(uint)0,
null!,
"0"
},
{
BuiltInType.UInt32,
(uint)1234567,
"1234567",
null!
},
{
BuiltInType.UInt32,
(uint)4444444,
"4444444",
null!
},
{
BuiltInType.UInt32,
uint.MinValue,
null!,
uint.MinValue.ToString(CultureInfo.InvariantCulture)
},
{
BuiltInType.UInt32,
uint.MaxValue,
uint.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Int32,
0,
null!,
"0"
},
{
BuiltInType.Int32,
-12345678,
"-12345678",
null!
},
{
BuiltInType.Int32,
12345678,
"12345678",
null!
},
{
BuiltInType.Int32,
int.MaxValue,
int.MaxValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Int32,
int.MinValue,
int.MinValue.ToString(CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Int64,
(long)0,
null!,
Quotes("0")
},
{
BuiltInType.Int64,
kInt64Value,
Quotes(kInt64Value.ToString(CultureInfo.InvariantCulture)),
null! },
{
BuiltInType.Int64,
(long)kUInt64Value,
Quotes(kUInt64Value.ToString(CultureInfo.InvariantCulture)),
null!
},
{
BuiltInType.Int64,
long.MinValue,
Quotes(long.MinValue.ToString(CultureInfo.InvariantCulture)),
null!
},
{
BuiltInType.Int64,
long.MaxValue,
Quotes(long.MaxValue.ToString(CultureInfo.InvariantCulture)),
null!
},
{
BuiltInType.UInt64,
(ulong)0,
null!,
Quotes("0")
},
{
BuiltInType.UInt64,
kUInt64Value,
Quotes(kUInt64Value.ToString(CultureInfo.InvariantCulture)),
null! },
{
BuiltInType.UInt64,
ulong.MinValue,
null!,
Quotes(ulong.MinValue.ToString(CultureInfo.InvariantCulture))
},
{
BuiltInType.UInt64,
ulong.MaxValue,
Quotes(ulong.MaxValue.ToString(CultureInfo.InvariantCulture)),
null!
},
{
BuiltInType.Float,
(float)0,
null!,
"0"
},
{
BuiltInType.Float,
(float)-12345678.1234,
Convert
.ToSingle("-12345678.1234", CultureInfo.InvariantCulture)
.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Float,
(float)12345678.1234,
Convert
.ToSingle("12345678.1234", CultureInfo.InvariantCulture)
.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Float,
float.MaxValue,
float.MaxValue.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Float,
float.MinValue,
float.MinValue.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Float,
float.NegativeInfinity,
Quotes("-Infinity"),
null!
},
{
BuiltInType.Float,
float.PositiveInfinity,
Quotes("Infinity"),
null!
},
{
BuiltInType.Float,
float.NaN,
Quotes("NaN"),
null!
},
{
BuiltInType.Double,
(double)0,
null!,
"0"
},
{
BuiltInType.Double,
-12345678.1234,
Convert
.ToDouble("-12345678.1234", CultureInfo.InvariantCulture)
.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Double,
12345678.1234,
Convert
.ToDouble("12345678.1234", CultureInfo.InvariantCulture)
.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Double,
double.MaxValue,
double.MaxValue.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Double,
double.MinValue,
double.MinValue.ToString("R", CultureInfo.InvariantCulture),
null!
},
{
BuiltInType.Double,
double.NegativeInfinity,
Quotes("-Infinity"),
null!
},
{
BuiltInType.Double,
double.PositiveInfinity,
Quotes("Infinity"),
null!
},
{
BuiltInType.Double,
double.NaN,
Quotes("NaN"),
null!
},
{
BuiltInType.DateTime,
DateTimeUtc.MinValue + 1,
Quotes("1601-01-01T00:00:00.001Z"),
null!
},
{
BuiltInType.DateTime,
DateTimeUtc.MinValue,
null!,
Quotes("0001-01-01T00:00:00Z")
},
{
BuiltInType.DateTime,
DateTimeUtc.MaxValue,
Quotes("9999-12-31T23:59:59Z"),
null!
},
{
BuiltInType.Guid,
Uuid.Empty,
null!,
"null" // Quotes("00000000-0000-0000-0000-000000000000")
},
{
BuiltInType.Guid,
s_nodeIdGuid,
Quotes($"{s_nodeIdGuid}"),
null!
},
{
BuiltInType.NodeId,
NodeId.Null,
null!,
"null"
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdInt),
$"""
"i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdInt, 1),
$"""
"nsu={kApplicationUri};i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdInt, kDemoServerIndex),
$"""
"nsu={kDemoServer};i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdInt, 88),
$"""
"ns=88;i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.NodeId,
NodeId.Parse("ns=0;s=" + kNodeIdString),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
NodeId.Parse("s=" + kNodeIdString),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdString, 0),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdString, 1),
$"""
"nsu={kApplicationUri};s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdString, kDemoServerIndex),
$"""
"nsu={kDemoServer};s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(kNodeIdString, 88),
$"""
"ns=88;s={kNodeIdString}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_nodeIdGuid),
$"""
"g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_nodeIdGuid, 1),
$"""
"nsu={kApplicationUri};g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_nodeIdGuid, kDemoServerIndex),
$"""
"nsu={kDemoServer};g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_nodeIdGuid, 88),
$"""
"ns=88;g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_byteString),
$"""
"b={s_byteString64}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_byteString, 1),
$"""
"nsu={kApplicationUri};b={s_byteString64}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_byteString, kDemoServerIndex),
$"""
"nsu={kDemoServer};b={s_byteString64}"
""",
null!
},
{
BuiltInType.NodeId,
new NodeId(s_byteString, 88),
$"""
"ns=88;b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
ExpandedNodeId.Null,
null!,
"null"
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdInt),
$"""
"i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdInt, 1),
$"""
"nsu={kApplicationUri};i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdInt, kDemoServerIndex),
$"""
"nsu={kDemoServer};i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdInt, kDemoServer2),
$"""
"nsu={kDemoServer2};i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdInt, 88),
$"""
"ns=88;i={kNodeIdInt}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
ExpandedNodeId.Parse("ns=0;s=" + kNodeIdString),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
ExpandedNodeId.Parse("s=" + kNodeIdString),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdString, 0),
$"""
"s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdString, 1),
$"""
"nsu={kApplicationUri};s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdString, kDemoServerIndex),
$"""
"nsu={kDemoServer};s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdString, kDemoServer2),
$"""
"nsu={kDemoServer2};s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(kNodeIdString, 88),
$"""
"ns=88;s={kNodeIdString}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_nodeIdGuid),
$"""
"g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_nodeIdGuid, 1),
$"""
"nsu={kApplicationUri};g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_nodeIdGuid, kDemoServerIndex),
$"""
"nsu={kDemoServer};g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_nodeIdGuid, kDemoServer2),
$"""
"nsu={kDemoServer2};g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_nodeIdGuid, 88),
$"""
"ns=88;g={s_nodeIdGuid}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_byteString),
$"""
"b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_byteString, 1),
$"""
"nsu={kApplicationUri};b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_byteString, kDemoServerIndex),
$"""
"nsu={kDemoServer};b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_byteString, kDemoServer2),
$"""
"nsu={kDemoServer2};b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(s_byteString, 88),
$"""
"ns=88;b={s_byteString64}"
""",
null!
},
{
BuiltInType.ExpandedNodeId,
new ExpandedNodeId(NodeId.Parse("ns=33;s=StringIdentifier"), null, 23),
"""
"svr=23;ns=33;s=StringIdentifier"
""", // compact
null! // verbose - null == same as compact
},
{
BuiltInType.StatusCode,
StatusCodes.Good,
null!,
"{}"
},
{
BuiltInType.StatusCode,
StatusCodes.BadBoundNotFound,
$$"""{"Code":{{StatusCodes.BadBoundNotFound.Code}}}""",
$$"""{"Code":{{StatusCodes.BadBoundNotFound.Code}}, "Symbol":"{{nameof(StatusCodes.BadBoundNotFound)}}"}"""
},
{
BuiltInType.StatusCode,
StatusCodes.BadCertificateInvalid,
$$"""{"Code":{{StatusCodes.BadCertificateInvalid.Code}}}""",
$$"""{"Code":{{StatusCodes.BadCertificateInvalid.Code}}, "Symbol":"{{nameof(StatusCodes.BadCertificateInvalid)}}"}"""
},
{
BuiltInType.StatusCode,
new StatusCode(1234567),
/*lang=json,strict*/ """{"Code":1234567}""",
null!
},
{
BuiltInType.QualifiedName,
QualifiedName.Null,
null!,
"null"
},
{
BuiltInType.QualifiedName,
QualifiedName.From(kQualifiedName),
$"""
"{kQualifiedName}"
""",
null!
},
{
BuiltInType.QualifiedName,
new QualifiedName(kQualifiedName, 1),
$"""
"nsu={kApplicationUri};{kQualifiedName}"
""",
null!
},
{
BuiltInType.QualifiedName,
new QualifiedName(kQualifiedName, kDemoServerIndex),
$"""
"nsu={kDemoServer};{kQualifiedName}"
""",
null!
},
{
BuiltInType.LocalizedText,
LocalizedText.Null,
null!,
"null"
},
{
BuiltInType.LocalizedText,
new LocalizedText(kLocalizedText),
$$"""{"Text":"{{kLocalizedText}}"}""",
null!
},
{
BuiltInType.LocalizedText,
new LocalizedText(kLocale, kLocalizedText),
$$"""{"Text":"{{kLocalizedText}}","Locale":"{{kLocale}}"}""",
null!
},
{
BuiltInType.LocalizedText,
new LocalizedText(kLocale, string.Empty),
// Part 6 5.4.2.15: an empty or null Text is not encoded.
$$"""{"Locale":"{{kLocale}}"}""",
null!
},
{
BuiltInType.LocalizedText,
new LocalizedText(kLocale, (string)null!),
$$"""{"Locale":"{{kLocale}}"}""",
null!
},
{
BuiltInType.ExtensionObject,
ExtensionObject.Null,
null!,
"null"
},
{
BuiltInType.ExtensionObject,
new ExtensionObject(kNodeIdInt),
/*lang=json,strict*/ """{"UaTypeId":"i=2345"}""",
null!
},
{
BuiltInType.ExtensionObject,
new ExtensionObject(null!),
null!,
"null"
},
{
BuiltInType.Variant,
Variant.Null,
null!,
"null"
},
{
BuiltInType.Variant,
new Variant((sbyte)123),
$$"""{"UaType":{{BuiltInType.SByte:d}}, "Value":123}""",
null!
},
{
BuiltInType.Variant,
new Variant((short)12345),
$$"""{"UaType":{{BuiltInType.Int16:d}}, "Value":12345}""",
null!
},
{
BuiltInType.Variant,
new Variant(1234567),
$$"""{"UaType":{{BuiltInType.Int32:d}}, "Value":1234567}""",
null!
},
{
BuiltInType.Variant,
new Variant((long)123456789),
$$"""{"UaType":{{BuiltInType.Int64:d}}, "Value":"123456789"}""",
null!
},
{
BuiltInType.Variant,
new Variant((byte)123),
$$"""{"UaType":{{BuiltInType.Byte:d}}, "Value":123}""",
null!
},
{
BuiltInType.Variant,
new Variant((ushort)12345),
$$"""{"UaType":{{BuiltInType.UInt16:d}}, "Value":12345}""",
null!
},
{
BuiltInType.Variant,
new Variant((uint)1234567),
$$"""{"UaType":{{BuiltInType.UInt32:d}}, "Value":1234567}""",
null!
},
{
BuiltInType.Variant,
new Variant((ulong)123456789),
$$"""{"UaType":{{BuiltInType.UInt64:d}}, "Value":"123456789"}""",
null!
},
{
BuiltInType.DataValue,
new DataValue(),
"{}",
null!
},
{
BuiltInType.DataValue,
DataValue.FromStatusCode(StatusCodes.Good),
"{}",
null!
},
{
BuiltInType.DataValue,
DataValue.FromStatusCode(StatusCodes.BadNotWritable),
$$$"""{"Status":{"Code":{{{StatusCodes.BadNotWritable.Code}}}}}""",
$$$"""
{
"Status": {
"Code": {{{StatusCodes.BadNotWritable.Code}}},
"Symbol": "{{{nameof(StatusCodes.BadNotWritable)}}}"
}
}
"""
},
{
BuiltInType.Enumeration,
Variant.From((TestEnumType)0),
null!, // "0",
"""
"0"
"""
},
{
BuiltInType.Enumeration,
Variant.From(TestEnumType.Three),
"""
3
""",
"""
"Three_3"
"""
},
{
BuiltInType.Enumeration,
Variant.From(TestEnumType.Ten),
"""
10
""",
"""
"Ten_10"
"""
},
{
BuiltInType.Enumeration,
Variant.From((TestEnumType)11),
"11",
"""
"11"
"""
},
{
BuiltInType.Enumeration,
Variant.From(new EnumValue(1)),
"1",
"""
"1"
"""
},
{
BuiltInType.Enumeration,
Variant.From(EnumValue.From((int)TestEnumType.Two)),
TestEnumType.Two.ToString("d"),
$"""
"{TestEnumType.Two:d}"
"""
},
{
BuiltInType.Enumeration,
Variant.From(EnumValue.From((int)TestEnumType.Hundred)),
$"{TestEnumType.Hundred:d}",
$"""
"{TestEnumType.Hundred:d}"
"""
},
{
BuiltInType.Enumeration,
Variant.From(EnumValue.From(22)),
"22",
"""
"22"
"""
},
// arrays
{
BuiltInType.Enumeration,
Variant.From(s_testEnumArray),
"[1,2,100]",
"""["One_1","Two_2","Hundred_100"]"""
},
{
BuiltInType.Enumeration,
Variant.From(EnumValue.From(s_testInt32Array)),
"[2,3,10]",
"""["2","3","10"]"""
},
// IEncodeable
{
BuiltInType.ExtensionObject,
s_testEncodeable,
/*lang=json,strict*/
"""{"Foo":"bar_999"}""",
null!
}
}.ToArray();
[DatapointSource]
public static readonly StatusCode[] GoodAndBadStatusCodes =
[
StatusCodes.Good,
StatusCodes.BadAlreadyExists
];
[OneTimeSetUp]
protected new void OneTimeSetUp()
{
ushort demoServerIndex = NameSpaceUris.GetIndexOrAppend(kDemoServer);
Assume.That(
demoServerIndex == kDemoServerIndex,
$"Server Index: {demoServerIndex} != {kDemoServerIndex}");
// for validating benchmark tests
m_telemetry = NUnitTelemetryContext.Create();
m_context = Ua.ServiceMessageContext.Create(m_telemetry);
m_memoryStream = new MemoryStream();
}
[OneTimeTearDown]
protected new void OneTimeTearDown()
{
m_memoryStream.Dispose();
}
[SetUp]
protected new void SetUp()
{
}
[TearDown]
protected new void TearDown()
{
}
/// <summary>
/// Set up some variables for benchmarks.
/// </summary>
[GlobalSetup]
public void GlobalSetup()
{
m_telemetry = NUnitTelemetryContext.Create();
m_context = Ua.ServiceMessageContext.Create(m_telemetry);
m_memoryStream = new MemoryStream();
}
/// <summary>
/// Tear down benchmark variables.
/// </summary>
[GlobalCleanup]
public void GlobalCleanup()
{
m_context = null!;
m_memoryStream.Dispose();
m_memoryStream = null!;
}
/// <summary>
/// Validate constructor signature.
/// </summary>
[Theory]
public void ConstructorDefault(JsonEncodingType encodingType)
{
var context = Ua.ServiceMessageContext.Create(m_telemetry);
using var jsonEncoder = new JsonEncoder(context,
encodingType == JsonEncodingType.Verbose ?
JsonEncoderOptions.Verbose :
JsonEncoderOptions.Compact);
TestEncoding(jsonEncoder);
string result = jsonEncoder.CloseAndReturnText();
Assert.That(result, Is.Not.Empty);
Assert.That(result, Is.Not.Null);
TestOutput.WriteLine("Result:");
_ = PrettifyAndValidateJson(result);
}
/// <summary>
/// Use a MemoryStream constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
[Test]
public void ConstructorMemoryStream()
{
using var memoryStream = new MemoryStream();
ConstructorStream(memoryStream);
}
/// <summary>
/// Use an external stream across multiple dispose cycles and keep output stable.
/// </summary>
[Test]
public void ConstructorExternalStreamProducesSameJsonAcrossDisposeCycles()
{
var context = Ua.ServiceMessageContext.Create(m_telemetry);
using var memoryStream = new MemoryStream();
string? expected = null;
for (int ii = 0; ii < 4; ii++)
{
memoryStream.SetLength(0);
memoryStream.Position = 0;
var jsonEncoder = new JsonEncoder(memoryStream, context, JsonEncoderOptions.Compact);
TestEncoding(jsonEncoder);
jsonEncoder.Dispose();
jsonEncoder.Dispose();
string encoded = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.That(encoded, Is.Not.Empty);
Assert.That(memoryStream.CanWrite, Is.True);
_ = PrettifyAndValidateJson(encoded);
if (expected == null)
{
expected = encoded;
}
else
{
Assert.That(encoded, Is.EqualTo(expected));
}
}
}
/// <summary>
/// Use a ArraySegmentStream constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
[Test]
public void ConstructorArraySegmentStream()
{
using var memoryStream = new ArraySegmentStream(BufferManager);
ConstructorStream(memoryStream);
}
/// <summary>
/// Use a constructor with external Stream,
/// keep the stream open for more encodings.
/// </summary>
private void ConstructorStream(MemoryStream memoryStream)
{
var context = Ua.ServiceMessageContext.Create(m_telemetry);
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
}
string result1 = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.That(result1, Is.Not.Empty);
TestOutput.WriteLine("Result1:");
_ = PrettifyAndValidateJson(result1);
// recycle the StreamWriter, ensure the result is equal
memoryStream.Position = 0;
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
}
string result2 = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.That(result2, Is.Not.Empty);
TestOutput.WriteLine("Result2:");
_ = PrettifyAndValidateJson(result2);
Assert.That(result2, Is.EqualTo(result1));
// recycle the StreamWriter, ensure the result is equal,
// use reflection to return result in external stream
memoryStream.Position = 0;
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
string result3 = jsonEncoder.CloseAndReturnText();
Assert.That(result3, Is.Not.Empty);
TestOutput.WriteLine("Result3:");
_ = PrettifyAndValidateJson(result3);
Assert.That(result3, Is.EqualTo(result1));
}
}
/// <summary>
/// Use a constructor with external ArraySegmentStream,
/// keep the stream open for more encodings.
/// Alternate use of sequence.
/// </summary>
[Test]
public void ConstructorArraySegmentStreamSequence()
{
var context = Ua.ServiceMessageContext.Create(m_telemetry);
using var memoryStream = new ArraySegmentStream(BufferManager);
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
}
// get the buffers and save the result
#if NET5_0_OR_GREATER
string result1;
using (BufferSequence sequence = memoryStream.GetSequence(nameof(ConstructorStream)))
{
result1 = Encoding.UTF8.GetString(sequence.Sequence);
Assert.IsNotEmpty(result1);
TestOutput.WriteLine("Result1:");
_ = PrettifyAndValidateJson(result1);
}
#else
string result1 = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.That(result1, Is.Not.Empty);
TestOutput.WriteLine("Result1:");
_ = PrettifyAndValidateJson(result1);
#endif
// recycle the memory stream, ensure the result is equal
memoryStream.Position = 0;
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
}
string result2 = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.That(result2, Is.Not.Empty);
TestOutput.WriteLine("Result2:");
_ = PrettifyAndValidateJson(result2);
Assert.That(result2, Is.EqualTo(result1));
// recycle the StreamWriter, ensure the result is equal,
// use reflection to return result in external stream
memoryStream.Position = 0;
using (var jsonEncoder = new JsonEncoder(memoryStream, context))
{
TestEncoding(jsonEncoder);
string result3 = jsonEncoder.CloseAndReturnText();
Assert.That(result3, Is.Not.Empty);
TestOutput.WriteLine("Result3:");
_ = PrettifyAndValidateJson(result3);
Assert.That(result3, Is.EqualTo(result1));
}
}
/// <summary>
/// Verify any Json encoding.
/// </summary>
[Theory]
public void JsonEncode(
JsonEncodingType jsonEncodingType,
JsonValidationData jsonValidationData,
MemoryStreamType memoryStreamType)
{
EncodeJsonVerifyResult(
jsonValidationData.BuiltInType,
memoryStreamType,
jsonValidationData.Instance,
jsonEncodingType,
jsonValidationData.GetExpected(jsonEncodingType));
}
/// <summary>
/// Within an object JSON don't allow another object without fieldname.
/// </summary>
[Test]
public void TestWriteSingleEncodeableWithoutName()
{
string expected = /*lang=json,strict*/ """{"Test":{"Foo":"bar_1"}}""";
TestOutput.WriteLine("Expected:");
expected = PrettifyAndValidateJson(expected);
using var encodeable = new FooBarEncodeable();
using var encoder = new JsonEncoder(Context);
encoder.WriteEncodeable("Test", encodeable);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
encoded = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// A single encodeable in an array cannot have a fieldname.
/// </summary>
[Test]
public void TestWriteSingleEncodeableWithName()
{
const string expected = /*lang=json,strict*/
"""{"bar_1":{"Foo":"bar_1"}}""";
TestOutput.WriteLine("Expected:");
_ = PrettifyAndValidateJson(expected);
using var encodeable = new FooBarEncodeable();
using var encoder = new JsonEncoder(Context);
encoder.WriteEncodeable(encodeable.Foo, encodeable);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
_ = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// A single dynamic encodeable
/// </summary>
[Test]
public void TestWriteSingleDynamicEncodeableWithName()
{
const string expected = /*lang=json,strict*/
"""{"bar_1":{"Foo":"bar_1"}}""";
TestOutput.WriteLine("Expected:");
_ = PrettifyAndValidateJson(expected);
using var encodeable = new DynamicEncodeable(
"FooXml",
"urn:dynamic_encoder_test",
ExpandedNodeId.Parse("ns=2;s=test_dyn_typeid"),
ExpandedNodeId.Parse("s=test_dyn_binaryencodingid"),
ExpandedNodeId.Parse("s=test_dyn_xmlencodingid"),
new Dictionary<string, (int, string)>
{
{ "Foo", (1, "bar_1") }
});
using var encoder = new JsonEncoder(Context);
encoder.WriteEncodeable("bar_1", encodeable);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
_ = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// Extension object with dynamic encodeable encoded to and from Json and xml
/// </summary>
[Test]
public void TestExtensionObjectWithDynamicEncodeable()
{
const string expectedJson = /*lang=json,strict*/
"""{"Test":{"UaTypeId":"s=test_dyn2_typeid","Foo":"bar_1","Foo2":"bar_2"}}""";
string expectedXml =
"""
<uax:ExtensionObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd">
<uax:TypeId><uax:Identifier>s=test_dyn2_xmlencodingid</uax:Identifier></uax:TypeId>
<uax:Body>
<FooXml xmlns="urn:dynamic_encoder_test">
<Foo>bar_1</Foo>
<Foo2>bar_2</Foo2>
</FooXml>
</uax:Body>
</uax:ExtensionObject>
""";
TestOutput.WriteLine("Expected XML:");
expectedXml = PrettifyAndValidateXml(Encoding.UTF8.GetBytes(expectedXml));
var encodeable = new DynamicEncodeable(
"FooXml",
"urn:dynamic_encoder_test",
ExpandedNodeId.Parse("s=test_dyn2_typeid"),
ExpandedNodeId.Parse("s=test_dyn2_binaryencodingid"),
ExpandedNodeId.Parse("s=test_dyn2_xmlencodingid"),
new Dictionary<string, (int, string)>
{
{ "Foo", (1, "bar_1") },
{ "Foo2", (2, "bar_2") }
});
// Register in the context's Factory, make it a custom factory so the dynamic type can
// look up its type information when instantiated during encoding/decoding
var dynamicContext = new ServiceMessageContext(
m_telemetry,
new DynamicEncodeableFactory(Context.Factory))
{
NamespaceUris = Context.NamespaceUris
};
(dynamicContext.Factory as DynamicEncodeableFactory)?.AddDynamicEncodeable(encodeable);
// Encode to XML: invokes IDynamicComplexTypeInstance.GetXmlName
string encodedXml;
using (var ms = new MemoryStream())
using (var xmlWriter = new XmlTextWriter(ms, Encoding.UTF8))
{
using (
var encoder = new XmlEncoder(
new XmlQualifiedName("uax:ExtensionObject", null),
xmlWriter,
Context))
{
var extensionObject = new ExtensionObject(encodeable);
encoder.WriteExtensionObject(null, extensionObject);
xmlWriter.Flush();
}
TestOutput.WriteLine("Formatted Encoded:");
encodedXml = PrettifyAndValidateXml(ms.ToArray());
}
Assert.That(encodedXml, Is.EqualTo(expectedXml));
// Decode from XML
ExtensionObject extensionObjectFromXml;
using (var ms2 = new MemoryStream(Encoding.UTF8.GetBytes(expectedXml)))
{
var xmlDoc = new XmlDocument();
var r = XmlReader.Create(
ms2,
new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore });
xmlDoc.Load(r);
using var decoder = new XmlDecoder((xmlDoc.FirstChild as System.Xml.XmlElement)!, dynamicContext);
decoder.PushNamespace(Namespaces.OpcUaXsd);
extensionObjectFromXml = decoder.ReadExtensionObject("ExtensionObject");
decoder.PopNamespace();
}
Assert.That(
extensionObjectFromXml.TryGetValue(out IEncodeable? resultEncodeable), Is.True);
Assert.That(encodeable.IsEqual(resultEncodeable!), Is.True);
// Encode to JSON
string encodedJson;
using (var encoder = new JsonEncoder(Context))
{
encoder.WriteExtensionObject("Test", extensionObjectFromXml);
encodedJson = encoder.CloseAndReturnText();
TestOutput.WriteLine("Expected Json:");
_ = PrettifyAndValidateJson(expectedJson);
TestOutput.WriteLine("Encoded Json:");
TestOutput.WriteLine(encodedJson);
TestOutput.WriteLine("Formatted Encoded Json:");
_ = PrettifyAndValidateJson(encodedJson);
}
Assert.That(encodedJson, Is.EqualTo(expectedJson));
// Decode from JSON: requires custom context
ExtensionObject extensionObjectFromJson;
using (var decoder = new JsonDecoder(encodedJson, dynamicContext))
{
extensionObjectFromJson = decoder.ReadExtensionObject("Test");
}
Assert.That(
extensionObjectFromJson.TryGetValue(out resultEncodeable!), Is.True);
Assert.That(encodeable.IsEqual(resultEncodeable!), Is.True);
}
/// <summary>
/// Write multiple encodeables with fieldnames.
/// </summary>
[Test]
public void TestWriteMultipleEncodeablesWithFieldNames()
{
const string expected = /*lang=json,strict*/
"""{"bar_1":{"Foo":"bar_1"},"bar_2":{"Foo":"bar_2"},"bar_3":{"Foo":"bar_3"}}""";
TestOutput.WriteLine("Expected:");
_ = PrettifyAndValidateJson(expected);
var encodeables = new List<FooBarEncodeable> { new(), new(), new() };
try
{
using var encoder = new JsonEncoder(Context);
foreach (FooBarEncodeable encodeable in encodeables)
{
encoder.WriteEncodeable(encodeable.Foo, encodeable);
}
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
_ = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
finally
{
encodeables.ForEach(e => e.Dispose());
}
}
/// <summary>
/// Write encodeable array with fieldname.
/// </summary>
[Test]
public void TestWriteEncodeableArrayWithFieldName()
{
var encodeables = new List<FooBarEncodeable> { new(), new(), new() };
RunWriteEncodeableArrayTest(
"array",
encodeables,
/*lang=json,strict*/
"""{"array":[{"Foo":"bar_1"},{"Foo":"bar_2"},{"Foo":"bar_3"}]}""",
false);
}
/// <summary>
/// Test if field names and values are properly escaped.
/// </summary>
[TestCase(
"""
"Hello"."World"
""",
"""
"Test"."Output"
""",
/*lang=json,strict*/
"""{"\"Hello\".\"World\"":{"\"Hello\".\"World\"":"\"Test\".\"Output\""}}"""
)]
[TestCase(
"\"Hello\".\"World\"\b\f\n\r\t\\",
"\"Test\b\f\n\r\t\\\".\"Output\"",
/*lang=json,strict*/
"""{"\"Hello\".\"World\"\b\f\n\r\t\\":{"\"Hello\".\"World\"\b\f\n\r\t\\":"\"Test\b\f\n\r\t\\\".\"Output\""}}"""
)]
public void TestFieldValueEscapedEncodeable(string fieldname, string foo, string expected)
{
TestOutput.WriteLine("Expected:");
expected = PrettifyAndValidateJson(expected);
using var encodeable = new FooBarEncodeable(fieldname, foo);
using var encoder = new JsonEncoder(Context, JsonEncoderOptions.Verbose);
encoder.WriteEncodeable(encodeable.FieldName, encodeable);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
encoded = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// Test if field names and values are properly escaped when used in an array.
/// </summary>
[TestCase(
"""
"Hello"."World"
""",
"""
"Test"."Output"
""",
/*lang=json,strict*/"""{"\"Hello\".\"World\"":[{"\"Hello\".\"World\"":"\"Test\".\"Output\""},{"\"Hello\".\"World\"":"\"Test\".\"Output\""}]}"""
)]
public void TestFieldValueEscapedArray(string fieldname, string foo, string expected)
{
TestOutput.WriteLine("Expected:");
expected = PrettifyAndValidateJson(expected);
using var encodeable = new FooBarEncodeable(fieldname, foo);
ArrayOf<FooBarEncodeable> list = [encodeable, encodeable];
using var encoder = new JsonEncoder(Context, JsonEncoderOptions.Verbose);
encoder.WriteEncodeableArray(encodeable.FieldName, list);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
encoded = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// Test if field names and values are properly escaped when used in a variant.
/// </summary>
[TestCase(
"""
"Hello"."World"
""",
"""
"Test"."Output"
""",
/*lang=json,strict*/"""{"\"Hello\".\"World\"":{"UaType":22,"Value":{"\"Hello\".\"World\"":"\"Test\".\"Output\""}}}"""
)]
public void TestFieldValueEscapedVariant(string fieldname, string foo, string expected)
{
TestOutput.WriteLine("Expected:");
expected = PrettifyAndValidateJson(expected);
using var encodeable = new FooBarEncodeable(fieldname, foo);
var variant = new Variant(new ExtensionObject(encodeable));
using var encoder = new JsonEncoder(Context, JsonEncoderOptions.Verbose);
encoder.WriteVariant(encodeable.FieldName, variant);
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
encoded = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
/// <summary>
/// Validate that the DateTime format strings return an equal result.
/// </summary>
[Test]
[Repeat(kRandomRepeats)]
public void DateTimeEncodeRandomStringTest()
{
SetRepeatedRandomSeed();
DateTimeUtc randomDateTime = DataGenerator.GetRandomDateTime();
DateTimeEncodeStringTest(randomDateTime);
}
/// <summary>
/// Validate that the DateTime format strings return an equal result.
/// </summary>
[Test]
[TestCase("1952-12-14T17:48:51.3559888Z")]
[TestCase("1952-12-14T17:48:51.3559880Z")]
[TestCase("1952-12-14T17:48:51.3559800Z")]
[TestCase("1952-12-14T17:48:51.3559000Z")]
[TestCase("1952-12-14T17:48:51.3550000Z")]
[TestCase("1952-12-14T17:48:51.3500000Z")]
[TestCase("1952-12-14T17:48:51.3000000Z")]
[TestCase("1952-12-14T17:48:51.0000000Z")]
[TestCase("1952-12-14T17:48:51Z")]
public void DateTimeEncodeStringTestCase(string dateTimeString)
{
bool success = DateTime.TryParse(
dateTimeString,
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal,
out DateTime dateTime);
Assert.That(success, Is.True);
DateTimeEncodeStringTest(dateTime);
}
/// <summary>
/// Validate that a ExpandedNodeId returns the expected
/// result for a not well formed Uri.
/// </summary>
[Test]
public void NotWellFormedUriInExpandedNodeId2String()
{
const string namespaceUri = "KEPServerEX";
const string nodeName = "Data Type Examples.16 Bit Device.K Registers.Double3";
string expectedNodeIdString = $"nsu={namespaceUri};s={nodeName}";
var expandedNodeId = ExpandedNodeId.Parse(expectedNodeIdString);
string stringifiedExpandedNodeId = expandedNodeId.ToString();
TestOutput.WriteLine(stringifiedExpandedNodeId);
Assert.That(stringifiedExpandedNodeId, Is.EqualTo(expectedNodeIdString));
}
/// <summary>
/// Validate that a statuscode in a DataValue produces valid JSON.
/// </summary>
[Theory]
public void DataValueWithStatusCodes(
JsonEncodingType jsonEncodingType,
[ValueSource(nameof(GoodAndBadStatusCodes))] StatusCode statusCodeVariant,
[ValueSource(nameof(GoodAndBadStatusCodes))] StatusCode statusCode)
{
var dataValue = new DataValue(
new Variant(statusCodeVariant),
statusCode,
DateTimeUtc.MinValue,
DateTime.UtcNow);
using var jsonEncoder = new JsonEncoder(
m_context,
jsonEncodingType == JsonEncodingType.Verbose ?
JsonEncoderOptions.Verbose :
JsonEncoderOptions.Compact);
jsonEncoder.WriteDataValue("Data", dataValue);
string result = jsonEncoder.CloseAndReturnText();
PrettifyAndValidateJson(result, true);
}
/// <summary>
/// Validate that the DateTime format strings return an equal result.
/// </summary>
private void DateTimeEncodeStringTest(DateTimeUtc testDateTime)
{
string resultString = testDateTime.ToString(
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK",
CultureInfo.InvariantCulture);
#if NET6_0_OR_GREATER
Span<char> valueString = stackalloc char[DateTimeHelper.DateTimeRoundTripKindLength];
DateTimeHelper.ConvertUniversalTimeToString(
(DateTime)testDateTime,
valueString,
out int charsWritten);
string resultO = valueString[..charsWritten].ToString();
#else
string resultO = DateTimeHelper.ConvertUniversalTimeToString((DateTime)testDateTime);
#endif
Assert.That(resultString, Is.Not.Null);
Assert.That(resultO, Is.Not.Null);
TestOutput.WriteLine(
"""Encoded: "o": {0} "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK": {1}""",
resultO,
resultString);
// last char is always 'Z', Utc time
Assert.That(resultString[^1], Is.EqualTo('Z'));
Assert.That(resultO[^1], Is.EqualTo('Z'));
Assert.That(resultO, Is.EqualTo(resultString));
var decodedXmlString = XmlConvert.ToDateTime(
resultString,
XmlDateTimeSerializationMode.Utc);
var decodedXmlO = XmlConvert.ToDateTime(resultO, XmlDateTimeSerializationMode.Utc);
Assert.That(decodedXmlString, Is.Not.EqualTo(DateTime.MinValue));
Assert.That(decodedXmlO, Is.Not.EqualTo(DateTime.MinValue));
TestOutput.WriteLine(
"Decoded Xml: {0} {1}",
decodedXmlO.ToString("o"),
decodedXmlString.ToString("o"));
Assert.That(Utils.IsEqual(decodedXmlString, decodedXmlO), Is.True);
// ensure decoded values are identical
bool successString = DateTime.TryParse(
resultString,
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal,
out DateTime decodedString);
bool successO = DateTime.TryParse(
resultO,
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal,
out DateTime decodedO);
Assert.That(successString, Is.True);
Assert.That(successO, Is.True);
TestOutput
.WriteLine("Decoded: {0} {1}", decodedO.ToString("o"), decodedString.ToString("o"));
Assert.That(decodedString, Is.EqualTo(decodedO));
Assert.That(decodedO, Is.EqualTo(testDateTime));
}
/// <summary>
/// Benchmark overhead to create ServiceMessageContext.
/// </summary>
[Benchmark]
[Test]
public void ServiceMessageContext()
{
_ = Ua.ServiceMessageContext.Create(m_telemetry);
}
/// <summary>
/// Benchmark overhead to create MemoryStream.
/// </summary>
[Benchmark]
[Test]
public void MemoryStream()
{
using var test = new MemoryStream();
_ = test.Length;
}
/// <summary>
/// Benchmark encoding with internal memory stream.
/// </summary>
[Benchmark]
[Test]
public void JsonEncoderConstructor()
{
using var jsonEncoder = new JsonEncoder(m_context, JsonEncoderOptions.Compact);
TestEncoding(jsonEncoder);
_ = jsonEncoder.CloseAndReturnText();
}
protected void TestEncoding(IEncoder encoder)
{
encoder.WriteBoolean("Boolean", true);
encoder.WriteUInt64("UInt64", 1234566890);
encoder.WriteString("String", "The quick brown fox...");
encoder.WriteNodeId("NodeId", new NodeId(1234, 3));
encoder.WriteInt32Array("Array", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
private void RunWriteEncodeableArrayTest(
string fieldName,
List<FooBarEncodeable> encodeables,
string expected,
bool noExpectedValidation = false)
{
try
{
if (!noExpectedValidation)
{
TestOutput.WriteLine("Expected:");
_ = PrettifyAndValidateJson(expected);
}
using var encoder = new JsonEncoder(Context, JsonEncoderOptions.Verbose);
encoder.WriteEncodeableArray(
fieldName,
encodeables.ToArrayOf());
string encoded = encoder.CloseAndReturnText();
TestOutput.WriteLine("Encoded:");
TestOutput.WriteLine(encoded);
TestOutput.WriteLine("Formatted Encoded:");
_ = PrettifyAndValidateJson(encoded);
Assert.That(encoded, Is.EqualTo(expected));
}
finally
{
encodeables.ForEach(e => e.Dispose());
}
}
private IServiceMessageContext m_context;
private MemoryStream m_memoryStream;
private ITelemetryContext m_telemetry;
}
}