Files
Marc Schier 9bfc780e14 Update NuGet dependencies and migrate tests to NUnit 5 (#4840)
# Description

Update 55 NuGet dependencies to the latest stable releases available on
2026-10-19, preserving documented compatibility pins and
framework-specific version bands. Existing OpenUSD prereleases remain
unchanged because no newer releases are available.

Migrate existing tests to NUnit 5: await asynchronous assertions,
propagate async through helpers, handle nullable predicates, and replace
obsolete `Order` attributes with sequencing dependencies. Sequencing
dependencies use `AllowFailure = true`; filtering a dependent test also
runs prerequisites. Adjust fixture-level parallelism where NUnit
dependency scheduling requires it.

Keep Roslyn API/runtime pins intact; select newer Immutable/Metadata
runtime packages only for test and profiling hosts requiring them.
Update the approved SDK pin to 10.0.402 across local, CI, and container
builds. Document NUnit 5 conventions.
2026-10-19 15:23:29 +00:00

354 lines
17 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.GMS.Contracts;
using Opc.Ua.GMS.Server;
using Opc.Ua.Machinery.Server;
using Opc.Ua.MachineTool;
namespace Opc.Ua.GMS.Tests
{
/// <summary>
/// Rejects malformed coherent observations through the public provider/handle boundary, not private validators.
/// </summary>
[TestFixture]
[Category("GMS")]
[Category("Integration")]
[NonParallelizable]
public sealed class GmsSystemValidationTests
{
/// <summary>
/// Starts one real owner for independent, sequentially disposed validation instances.
/// </summary>
[OneTimeSetUp]
public async Task StartAsync()
{
m_fixture = new GmsResultServerFixture();
await m_fixture.StartAsync().ConfigureAwait(false);
}
/// <summary>
/// Releases the real listener and all test-owned PKI resources.
/// </summary>
[OneTimeTearDown]
public async Task StopAsync()
{
if (m_fixture != null)
{
await m_fixture.DisposeAsync().ConfigureAwait(false);
}
}
/// <summary>
/// Neither native nor inherited values, identities or calibration history change when any field is rejected.
/// </summary>
[TestCase("MissingQualification")]
[TestCase("MissingClass")]
[TestCase("SignedClass")]
[TestCase("ClassRange")]
[TestCase("ClassNoTimestamp")]
[TestCase("WrongQualifiedType")]
[TestCase("ExpiredButQualified")]
[TestCase("InfiniteQualification")]
[TestCase("InvalidAlignment")]
[TestCase("WrongAxes")]
[TestCase("WrongCapability")]
[TestCase("CapabilityRange")]
[TestCase("NegativeRange")]
[TestCase("NegativeResolution")]
[TestCase("WrongUnitStructure")]
[TestCase("NestedSingleSensor")]
[TestCase("DuplicateSensor")]
[TestCase("ForeignToolKind")]
[TestCase("UnsignedLoadingRange")]
[TestCase("SignedLoading")]
[TestCase("WrongLoadingPosition")]
[TestCase("MissingActiveSensor")]
[TestCase("DuplicateActiveSensor")]
[TestCase("ForeignActiveNode")]
[TestCase("AmbiguousActiveTools")]
[TestCase("NegativeWorkspace")]
[TestCase("InfiniteWorkspace")]
[TestCase("AbstractWorkspace")]
[TestCase("MissingSubclass")]
[TestCase("OverlongSubclass")]
[TestCase("MissingCalibrationDate")]
[TestCase("FutureCalibration")]
[TestCase("BackdatedCalibration")]
[TestCase("OldPrediction")]
[TestCase("ZeroInterval")]
[TestCase("NegativePreparation")]
[TestCase("ExcessivePreparation")]
[TestCase("RelativeCertificate")]
[TestCase("WrongCertificates")]
[TestCase("InheritedBadMode")]
public async Task InvalidNativeObservationPreservesAcceptedRevisionAsync(string mistake)
{
IMachineryBuildContext context = GmsSystemTests.Context(m_fixture!);
GmsSystemSnapshot initial = GmsCalibrationTests.Example();
var provider = new InMemoryGmsProvider(initial);
await using GmsHandle handle = await GmsSystemTests.Builder(context, "Validation" + mistake, provider)
.WithOptions(new GmsSystemOptions
{
Features = GmsFeatures.Sensors | GmsFeatures.Identification | GmsFeatures.Loading |
GmsFeatures.ToolMonitoring | GmsFeatures.Calibration,
MaximumTextLength = 128
}).WithResults(new InMemoryGmsResultStore(ModelTestContext.CreateMessageContext()))
.BuildAsync().ConfigureAwait(false);
GmsSystemSnapshot invalid = Change(initial, mistake);
await provider.SetSnapshotAsync(invalid).ConfigureAwait(false);
ServiceResultException error = (await Assert.ThrowsAsync<ServiceResultException>(async () =>
await handle.RefreshAsync().ConfigureAwait(false)))!;
Assert.That(StatusCode.IsBad(error.StatusCode), Is.True);
Assert.That(handle.Observation, Is.SameAs(initial));
Assert.That(handle.CalibrationHistory.Count, Is.EqualTo(1));
Assert.That(handle.TryGetSensor("Probe", out BaseToolState? sensor), Is.True);
NodeId original = sensor!.NodeId;
await provider.SetSnapshotAsync(initial with
{
Calibration = initial.Calibration! with
{
Calibrated = GmsEquipmentTests.Sample(Variant.From(false))
}
}).ConfigureAwait(false);
await handle.RefreshAsync().ConfigureAwait(false);
Assert.That(handle.TryGetSensor("Probe", out sensor), Is.True);
Assert.That(sensor!.NodeId, Is.EqualTo(original));
Assert.That(handle.CalibrationHistory.Count, Is.EqualTo(2));
}
/// <summary>
/// Explicit optional selection cannot silently create an empty successful provider surface.
/// </summary>
[TestCase(GmsFeatures.Identification)]
[TestCase(GmsFeatures.Sensors)]
[TestCase(GmsFeatures.Loading)]
[TestCase(GmsFeatures.Accessories)]
[TestCase(GmsFeatures.AdditionalSensors)]
[TestCase(GmsFeatures.Calibration)]
public async Task MissingSelectedObservationFailsBeforePublication(GmsFeatures feature)
{
IMachineryBuildContext context = GmsSystemTests.Context(m_fixture!);
GmsBuilder builder = GmsSystemTests.Builder(context, "Missing" + feature,
new InMemoryGmsProvider(new GmsSystemSnapshot()))
.WithOptions(new GmsSystemOptions { Features = feature })
.WithResults(new InMemoryGmsResultStore(ModelTestContext.CreateMessageContext()));
await Assert.ThrowsAsync<ServiceResultException>(async () => await builder.BuildAsync().ConfigureAwait(false));
Assert.That(context.MachinesFolder.FindChild(context.Context,
new QualifiedName("Missing" + feature, context.InstanceNamespaceIndex)), Is.Null);
}
/// <summary>
/// Native configuration dependencies are checked before any provider acquisition or root allocation.
/// </summary>
[TestCase(GmsFeatures.DynamicSensors)]
[TestCase(GmsFeatures.DynamicProduction)]
[TestCase(GmsFeatures.ToolMonitoring)]
[TestCase(GmsFeatures.SensorWarnings)]
[TestCase(GmsFeatures.Sensors | GmsFeatures.SensorWarnings)]
[TestCase((GmsFeatures)4096)]
public void InvalidFeatureDependenciesFailConfiguration(GmsFeatures features)
{
GmsBuilder builder = GmsSystemTests.Builder(GmsSystemTests.Context(m_fixture!), "BadOptions",
new InMemoryGmsProvider(new GmsSystemSnapshot()));
ServiceResultException error = Assert.Throws<ServiceResultException>(() =>
builder.WithOptions(new GmsSystemOptions { Features = features }))!;
Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadConfigurationError));
}
/// <summary>
/// Constructs a single deliberately invalid field while preserving all unrelated valid observations.
/// </summary>
private static GmsSystemSnapshot Change(GmsSystemSnapshot snapshot, string mistake)
{
GmsSensor sensor = snapshot.Sensors[0];
GmsCalibrationObservation calibration = snapshot.Calibration!;
DataValue Sample(Variant value) => GmsEquipmentTests.Sample(value);
GmsSystemSnapshot Sensor(GmsSensor value) => snapshot with { Sensors = [value] };
GmsSystemSnapshot Calibration(GmsCalibrationObservation value) => snapshot with { Calibration = value };
return mistake switch
{
"MissingQualification" => Sensor(sensor with { Qualified = null }),
"MissingClass" => Sensor(sensor with { Class = DataValue.Null }),
"SignedClass" => Sensor(sensor with { Class = Sample(Variant.From(3)) }),
"ClassRange" => Sensor(sensor with { Class = Sample(Variant.From(ulong.MaxValue)) }),
"ClassNoTimestamp" => Sensor(sensor with { Class = new DataValue(Variant.From(3u)) }),
"WrongQualifiedType" => Sensor(sensor with
{
Qualified = sensor.Qualified! with { RemainingHours = Sample(Variant.From(1)) }
}),
"ExpiredButQualified" => Sensor(sensor with
{
Qualified = sensor.Qualified! with { RemainingHours = Sample(Variant.From(0.0)) }
}),
"InfiniteQualification" => Sensor(sensor with
{
Qualified = sensor.Qualified! with
{
RemainingHours = Sample(Variant.From(double.PositiveInfinity))
}
}),
"InvalidAlignment" => Sensor(sensor with { Alignment = Sample(Variant.From((ToolAlignmentState)3)) }),
"WrongAxes" => Sensor(sensor with { Axes = Sample(Variant.From("X")) }),
"WrongCapability" => Sensor(sensor with { Capabilities = Sample(Variant.From((ArrayOf<int>)[1])) }),
"CapabilityRange" => Sensor(sensor with { Capabilities = Sample(Variant.From((ArrayOf<uint>)[6])) }),
"NegativeRange" => Sensor(sensor with { MeasuringRange = Sample(Variant.From(-1.0)) }),
"NegativeResolution" => Sensor(sensor with { Resolution = Sample(Variant.From(-0.1)) }),
"WrongUnitStructure" => Sensor(sensor with
{
EngineeringUnit = Sample(Variant.FromStructure(new CartesianWorkspaceType()))
}),
"NestedSingleSensor" => Sensor(sensor with { Sensors = [GmsEquipmentTests.Sensor("Child")] }),
"DuplicateSensor" => snapshot with { Sensors = [sensor, sensor] },
"ForeignToolKind" => Sensor(sensor with
{
Tool = sensor.Tool with { Kind = MachineToolToolKind.Companion }
}),
"UnsignedLoadingRange" => snapshot with
{
Loading = snapshot.Loading! with { LoadStatus = Sample(Variant.From(4u)) }
},
"SignedLoading" => snapshot with
{
Loading = snapshot.Loading! with { LoadStatus = Sample(Variant.From(2)) }
},
"WrongLoadingPosition" => snapshot with
{
Loading = snapshot.Loading! with { IsInLoadingPosition = Sample(Variant.From("false")) }
},
"MissingActiveSensor" => snapshot with
{
ToolMonitoring = snapshot.ToolMonitoring! with { ActiveSensorKeys = ["OtherMachine"] }
},
"DuplicateActiveSensor" => snapshot with
{
ToolMonitoring = snapshot.ToolMonitoring! with { ActiveSensorKeys = ["Probe", "Probe"] }
},
"ForeignActiveNode" => snapshot with
{
ToolMonitoring = snapshot.ToolMonitoring! with
{
ActiveSensorKeys = default,
ActiveToolIds = Sample(Variant.From((ArrayOf<NodeId>)[Opc.Ua.ObjectIds.ObjectsFolder]))
}
},
"AmbiguousActiveTools" => snapshot with
{
ToolMonitoring = snapshot.ToolMonitoring! with
{
ActiveToolIds = Sample(Variant.From((ArrayOf<NodeId>)[]))
}
},
"NegativeWorkspace" => snapshot with
{
Identification = snapshot.Identification! with
{
Workspace = Sample(Variant.FromStructure(new CartesianWorkspaceType { Length = -1 }))
}
},
"InfiniteWorkspace" => snapshot with
{
Identification = snapshot.Identification! with
{
Workspace = Sample(Variant.FromStructure(new CylindricalWorkspaceType { Radius = double.NaN }))
}
},
"AbstractWorkspace" => snapshot with
{
Identification = snapshot.Identification! with
{
Workspace = Sample(Variant.FromStructure(new WorkspaceType()))
}
},
"MissingSubclass" => snapshot with
{
Identification = snapshot.Identification! with { SubDeviceClass = DataValue.Null }
},
"OverlongSubclass" => snapshot with
{
Identification = snapshot.Identification! with
{
SubDeviceClass = Sample(Variant.From(new string('x', 129)))
}
},
"MissingCalibrationDate" => Calibration(calibration with
{
DateOfCalibration = new DataValue(Variant.Null, StatusCodes.BadNoDataAvailable)
}),
"FutureCalibration" => Calibration(calibration with
{
DateOfCalibration = Sample(Variant.From(
new DateTimeUtc(new DateTime(2026, 11, 1, 0, 0, 0, DateTimeKind.Utc))))
}),
"BackdatedCalibration" => Calibration(calibration with
{
DateOfCalibration = Sample(Variant.From(
new DateTimeUtc(new DateTime(2026, 8, 1, 0, 0, 0, DateTimeKind.Utc))))
}),
"OldPrediction" => Calibration(calibration with
{
PredictedTime = Sample(Variant.From(
new DateTimeUtc(new DateTime(2026, 8, 1, 0, 0, 0, DateTimeKind.Utc))))
}),
"ZeroInterval" => Calibration(calibration with { CalibrationInterval = Sample(Variant.From(0.0)) }),
"NegativePreparation" => Calibration(calibration with
{
CalibrationPreptime = Sample(Variant.From(-1.0))
}),
"ExcessivePreparation" => Calibration(calibration with
{
CalibrationPreptime = Sample(Variant.From(1e15))
}),
"RelativeCertificate" => Calibration(calibration with
{
CalibrationCertificate = Sample(Variant.From((ArrayOf<string>)["relative.pdf"]))
}),
"WrongCertificates" => Calibration(calibration with
{
CalibrationCertificate = Sample(Variant.From("uri"))
}),
"InheritedBadMode" => snapshot with
{
MachineTool = snapshot.MachineTool with
{
Operation = new MachineToolOperation { Mode = (MachineOperationMode)99 }
}
},
_ => throw new ArgumentOutOfRangeException(nameof(mistake))
};
}
/// <summary>The real shared-owner fixture, reused only across sequential validation cases.</summary>
private GmsResultServerFixture? m_fixture;
}
}