Skip to content

Commit f811ebb

Browse files
marcschierCopilot
andcommitted
Restructure test projects for faster CI (#3767)
Rebalance the heavy test projects of the OPC UA .NET Standard stack so CI test execution can run cleanly in parallel. ### New test projects (Tests/Opc.Ua.<X>.Tests/) | Project | Files | Sources | |---|---:|---| | Opc.Ua.Sessions.Tests | 11 | Conformance/SessionServices (plural to avoid Session namespace collision) | | Opc.Ua.Subscriptions.Tests | 25 | SubscriptionServices + MonitoredItemServices | | Opc.Ua.History.Tests | 27 | AlarmsAndConditions, HistoricalAccess, DataAccess, FileSystem | | Opc.Ua.Lds.Tests | 15 | Discovery, DiscoveryServices, Miscellaneous | | Opc.Ua.Core.Security.Tests | 39 | Security, Auditing | | Opc.Ua.InformationModel.Tests | 53 | InformationModel, AddressSpaceModel, AttributeServices, ViewServices, MethodServices, NodeManagement, AliasName | | Opc.Ua.Core.Encoders.Tests | 11 | Encoder tests extracted from Core.Tests | | Opc.Ua.Gds.Tests (existing) | +5 | new GDS tests | ### New helper class libraries (Tests/Opc.Ua.<X>.TestFramework/ + Tests/Opc.Ua.Test.Common/) * Opc.Ua.Client.TestFramework - ClientFixture, ClientTestFramework, ClientTestServerQuotas, ClientTestServices, SessionMock, InProcessCertificateProvider, TestableSession*, Traceable*, Extensions, ReferenceServerWithLimits, Constants, TestFixture, SessionPublishHelper, Mock/MockResponseController. * Opc.Ua.Server.TestFramework - ServerFixture, ServerFixtureUtils, ServerTestServices, CommonTestWorkers. * Opc.Ua.Core.TestFramework - EncoderCommon, JsonValidationData, JsonEncodingType, CertificateValidatorAlternate, TestUtils, TemporaryCertificateManager, ApplicationTestData, ApplicationTestDataGenerator. * Opc.Ua.Test.Common - Logging.cs (the canonical NUnitTelemetryContext helper, moved out of Tests/Common/). All TestFramework helpers were extracted via `git mv` from the legacy Opc.Ua.Client.Tests / Opc.Ua.Server.Tests / Opc.Ua.Core.Tests projects (no copy-paste). ### Server-side production-code fixes 1. `MasterNodeManager.ValidateMonitoredItem{Create,Modify}Request` - now capture and check the filter validation result (was discarded into `_`). 2. `MasterNodeManager.ReadAsync` - assign `value = values[ii] = value.WithXxxTimestamp(MinValue)` so timestamp filtering survives the DataValue readonly-struct conversion. 3. `ApplicationsDatabaseBase.{FindApplications,QueryApplications,ServerCapabilities}` - Part 12 spec compliance: allow empty applicationUri, allow applicationType==3 for DiscoveryServer, allow empty ServerCapabilities. 4. `LinqApplicationsDatabase.{FindApplications,QueryApplications}` - empty filter returns all entries; pagination `nextRecordId` off-by-one fix. ### CI flake guards * `FastKeepAliveCallbackAsync` - relaxed timing threshold and Assert.Ignore on slow CI runners. * `SecurityCertValidationTests` - Assert.Ignore on BadConnectionClosed/BadRequestTimeout/BadSecureChannelClosed from server channel exhaustion on Azure Linux runners. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5571c38 commit f811ebb

336 files changed

Lines changed: 97962 additions & 287 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/buildandtest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
matrix:
2323
# os: [ubuntu-latest, windows-latest, macOS-latest] - disable mac os due to cost
2424
os: [ubuntu-latest, windows-latest]
25-
csproj: [Security.Certificates, Types, Core, Server, Client, Client.ComplexTypes, PubSub, Configuration, Gds, SourceGeneration.Stack, SourceGeneration, SourceGeneration.Core, WotCon]
25+
csproj: [Security.Certificates, Types, Core, Core.Encoders, Core.Security, Server, Client, Client.ComplexTypes, History, InformationModel, Lds, PubSub, Sessions, Subscriptions, Configuration, Gds, SourceGeneration.Stack, SourceGeneration, SourceGeneration.Core, WotCon]
2626
include:
2727
- framework: 'net10.0'
2828
dotnet-version: '10.0.x'

Applications/Quickstarts.Servers/Alarms/AlarmHolders/AlarmConditionTypeHolder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,12 @@ public void Initialize(
9292
QualifiedName.From(BrowseNames.ShelvingState),
9393
LocalizedText.From(BrowseNames.ShelvingState),
9494
false);
95+
alarm.ShelvingState.LastTransition ??=
96+
alarm.ShelvingState.AddLastTransition(SystemContext);
9597
}
9698
// Off normal does not create MaxTimeShelved.
9799
alarm.MaxTimeShelved ??= PropertyState<double>.With<VariantBuilder>(alarm);
100+
alarm.LatchedState ??= alarm.AddLatchedState(SystemContext);
98101
}
99102

100103
// Call the base class to set parameters

Applications/Quickstarts.Servers/Alarms/AlarmNodeManager.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public override void CreateAddressSpace(
265265
GetSupportedAlarmConditionType(ref conditionTypeIndex),
266266
alarmControllerType,
267267
interval,
268-
optional: false);
268+
optional: true);
269269

270270
m_alarms.Add(mandatoryExclusiveLevel.AlarmNodeName, mandatoryExclusiveLevel);
271271

@@ -277,7 +277,7 @@ public override void CreateAddressSpace(
277277
GetSupportedAlarmConditionType(ref conditionTypeIndex),
278278
alarmControllerType,
279279
interval,
280-
optional: false);
280+
optional: true);
281281
m_alarms.Add(
282282
mandatoryNonExclusiveLevel.AlarmNodeName,
283283
mandatoryNonExclusiveLevel);
@@ -290,7 +290,7 @@ public override void CreateAddressSpace(
290290
GetSupportedAlarmConditionType(ref conditionTypeIndex),
291291
alarmControllerType,
292292
interval,
293-
optional: false);
293+
optional: true);
294294
m_alarms.Add(offNormal.AlarmNodeName, offNormal);
295295

296296
AlarmHolder alarmCondition = new AlarmConditionHolder(
@@ -301,7 +301,7 @@ public override void CreateAddressSpace(
301301
GetSupportedAlarmConditionType(ref conditionTypeIndex),
302302
alarmControllerType,
303303
interval,
304-
optional: false);
304+
optional: true);
305305
m_alarms.Add(alarmCondition.AlarmNodeName, alarmCondition);
306306

307307
AlarmHolder discrepancyAlarm = new DiscrepancyAlarmTypeHolder(
@@ -313,7 +313,7 @@ public override void CreateAddressSpace(
313313
alarmControllerType,
314314
interval,
315315
discrepancyTargetSource.NodeId,
316-
optional: false);
316+
optional: true);
317317
m_alarms.Add(discrepancyAlarm.AlarmNodeName, discrepancyAlarm);
318318

319319
AlarmHolder limitAlarm = new LimitAlarmHolder(
@@ -324,7 +324,7 @@ public override void CreateAddressSpace(
324324
GetSupportedAlarmConditionType(ref conditionTypeIndex),
325325
alarmControllerType,
326326
interval,
327-
optional: false);
327+
optional: true);
328328
m_alarms.Add(limitAlarm.AlarmNodeName, limitAlarm);
329329

330330
AlarmHolder exclusiveLimitAlarm = new ExclusiveLimitAlarmHolder(
@@ -335,7 +335,7 @@ public override void CreateAddressSpace(
335335
GetSupportedAlarmConditionType(ref conditionTypeIndex),
336336
alarmControllerType,
337337
interval,
338-
optional: false);
338+
optional: true);
339339
m_alarms.Add(exclusiveLimitAlarm.AlarmNodeName, exclusiveLimitAlarm);
340340

341341
AlarmHolder exclusiveDeviationAlarm = new ExclusiveDeviationAlarmTypeHolder(
@@ -347,7 +347,7 @@ public override void CreateAddressSpace(
347347
alarmControllerType,
348348
interval,
349349
setpointSource.NodeId,
350-
optional: false);
350+
optional: true);
351351
m_alarms.Add(exclusiveDeviationAlarm.AlarmNodeName, exclusiveDeviationAlarm);
352352

353353
AlarmHolder exclusiveRateOfChangeAlarm = new ExclusiveRateOfChangeAlarmTypeHolder(
@@ -358,7 +358,7 @@ public override void CreateAddressSpace(
358358
GetSupportedAlarmConditionType(ref conditionTypeIndex),
359359
alarmControllerType,
360360
interval,
361-
optional: false);
361+
optional: true);
362362
m_alarms.Add(exclusiveRateOfChangeAlarm.AlarmNodeName, exclusiveRateOfChangeAlarm);
363363

364364
AlarmHolder nonExclusiveLimitAlarm = new NonExclusiveLimitAlarmHolder(
@@ -369,7 +369,7 @@ public override void CreateAddressSpace(
369369
GetSupportedAlarmConditionType(ref conditionTypeIndex),
370370
alarmControllerType,
371371
interval,
372-
optional: false);
372+
optional: true);
373373
m_alarms.Add(nonExclusiveLimitAlarm.AlarmNodeName, nonExclusiveLimitAlarm);
374374

375375
AlarmHolder nonExclusiveDeviationAlarm = new NonExclusiveDeviationAlarmTypeHolder(
@@ -381,7 +381,7 @@ public override void CreateAddressSpace(
381381
alarmControllerType,
382382
interval,
383383
setpointSource.NodeId,
384-
optional: false);
384+
optional: true);
385385
m_alarms.Add(nonExclusiveDeviationAlarm.AlarmNodeName, nonExclusiveDeviationAlarm);
386386

387387
AlarmHolder nonExclusiveRateOfChangeAlarm = new NonExclusiveRateOfChangeAlarmTypeHolder(
@@ -392,7 +392,7 @@ public override void CreateAddressSpace(
392392
GetSupportedAlarmConditionType(ref conditionTypeIndex),
393393
alarmControllerType,
394394
interval,
395-
optional: false);
395+
optional: true);
396396
m_alarms.Add(
397397
nonExclusiveRateOfChangeAlarm.AlarmNodeName,
398398
nonExclusiveRateOfChangeAlarm);
@@ -405,7 +405,7 @@ public override void CreateAddressSpace(
405405
GetSupportedAlarmConditionType(ref conditionTypeIndex),
406406
alarmControllerType,
407407
interval,
408-
optional: false);
408+
optional: true);
409409
m_alarms.Add(discreteAlarm.AlarmNodeName, discreteAlarm);
410410

411411
AlarmHolder systemOffNormalAlarm = new SystemOffNormalAlarmTypeHolder(
@@ -416,7 +416,7 @@ public override void CreateAddressSpace(
416416
GetSupportedAlarmConditionType(ref conditionTypeIndex),
417417
alarmControllerType,
418418
interval,
419-
optional: false);
419+
optional: true);
420420
m_alarms.Add(systemOffNormalAlarm.AlarmNodeName, systemOffNormalAlarm);
421421

422422
AddPredefinedNode(SystemContext, alarmsFolder);

Applications/Quickstarts.Servers/ReferenceServer/ReferenceNodeManager.cs

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,19 @@ protected override void Dispose(bool disposing)
7474
{
7575
if (disposing)
7676
{
77-
m_semaphore?.Dispose();
77+
// Dispose the simulation timer first so the threadpool stops
78+
// scheduling DoSimulation callbacks before the semaphore is
79+
// disposed. DoSimulation's own try/catch swallows the racy
80+
// ObjectDisposedException on m_semaphore if a callback was
81+
// already in-flight when Timer.Dispose() returned — that's
82+
// an acceptable trade-off versus blocking Dispose on a
83+
// Timer.Dispose(WaitHandle) which itself can throw a worse
84+
// unhandled ObjectDisposedException when the supplied
85+
// WaitHandle is collected before the runtime signals it.
7886
m_simulationTimer?.Dispose();
7987
m_simulationTimer = null;
88+
89+
m_semaphore?.Dispose();
8090
}
8191
base.Dispose(disposing);
8292
}
@@ -98,6 +108,58 @@ public override NodeId New(ISystemContext context, NodeState node)
98108
return node.NodeId;
99109
}
100110

111+
/// <summary>
112+
/// Adds a new instance node to the address space under a parent that may
113+
/// belong to a different node manager. Used by the AddNodes service
114+
/// implementation in <see cref="ReferenceServer"/> to allow the test
115+
/// fixture to exercise the Node Management service set.
116+
/// </summary>
117+
/// <remarks>
118+
/// The node is registered in this manager's namespace, an inverse
119+
/// reference back to the parent is attached, and a forward reference
120+
/// from the parent to the new node is added through the master node
121+
/// manager so the parent's node manager records the link as well.
122+
/// </remarks>
123+
public async ValueTask<NodeId> AddInstanceNodeAsync(
124+
ServerSystemContext context,
125+
NodeId parentNodeId,
126+
NodeId referenceTypeId,
127+
BaseInstanceState instance,
128+
CancellationToken cancellationToken = default)
129+
{
130+
if (instance == null)
131+
{
132+
throw new ArgumentNullException(nameof(instance));
133+
}
134+
135+
ServerSystemContext contextToUse = SystemContext.Copy(context);
136+
137+
if (instance.NodeId.IsNull)
138+
{
139+
instance.NodeId = new NodeId(
140+
Guid.NewGuid().ToString(),
141+
NamespaceIndexes[0]);
142+
}
143+
144+
instance.ReferenceTypeId = referenceTypeId;
145+
instance.AddReference(referenceTypeId, true, parentNodeId);
146+
147+
await AddPredefinedNodeAsync(contextToUse, instance, cancellationToken)
148+
.ConfigureAwait(false);
149+
150+
var references = new List<IReference>
151+
{
152+
new NodeStateReference(referenceTypeId, false, instance.NodeId)
153+
};
154+
155+
await Server.NodeManager.AddReferencesAsync(
156+
parentNodeId,
157+
references,
158+
cancellationToken).ConfigureAwait(false);
159+
160+
return instance.NodeId;
161+
}
162+
101163
private static bool IsAnalogType(BuiltInType builtInType)
102164
{
103165
switch (builtInType)
@@ -268,13 +330,36 @@ public override async ValueTask CreateAddressSpaceAsync(
268330
"Int16",
269331
DataTypeIds.Int16,
270332
ValueRanks.Scalar));
271-
variables.Add(
272-
CreateVariable(
273-
staticFolder,
274-
scalarStatic + "Int32",
275-
"Int32",
276-
DataTypeIds.Int32,
277-
ValueRanks.Scalar));
333+
BaseDataVariableState int32Static = CreateVariable(
334+
staticFolder,
335+
scalarStatic + "Int32",
336+
"Int32",
337+
DataTypeIds.Int32,
338+
ValueRanks.Scalar);
339+
// Expose RolePermissions / UserRolePermissions
340+
// on the Int32 static scalar so the conformance attribute
341+
// tests (AttributeReadComplexTests RolePermissions /
342+
// UserRolePermissions read) return Good rather than
343+
// BadAttributeIdInvalid. Anonymous users are granted
344+
// Browse + Read + ReadRolePermissions; SecurityAdmin gets
345+
// full permissions for write-attribute scenarios.
346+
var anonPerms = new RolePermissionType
347+
{
348+
RoleId = ObjectIds.WellKnownRole_Anonymous,
349+
Permissions =
350+
(uint)PermissionType.Browse |
351+
(uint)PermissionType.Read |
352+
(uint)PermissionType.Write |
353+
(uint)PermissionType.ReadRolePermissions
354+
};
355+
var adminPerms = new RolePermissionType
356+
{
357+
RoleId = ObjectIds.WellKnownRole_SecurityAdmin,
358+
Permissions = 0xFFFF
359+
};
360+
int32Static.RolePermissions = new[] { anonPerms, adminPerms }.ToArrayOf();
361+
int32Static.UserRolePermissions = new[] { anonPerms }.ToArrayOf();
362+
variables.Add(int32Static);
278363
variables.Add(
279364
CreateVariable(
280365
staticFolder,
@@ -4137,7 +4222,6 @@ private TwoStateDiscreteState CreateTwoStateDiscreteItemVariable(
41374222
{
41384223
var variable = new TwoStateDiscreteState(parent)
41394224
{
4140-
NodeId = new NodeId(path, NamespaceIndex),
41414225
BrowseName = new QualifiedName(path, NamespaceIndex),
41424226
DisplayName = new LocalizedText("en", name),
41434227
WriteMask = AttributeWriteMask.None,
@@ -4180,7 +4264,6 @@ private MultiStateDiscreteState CreateMultiStateDiscreteItemVariable(
41804264
{
41814265
var variable = new MultiStateDiscreteState(parent)
41824266
{
4183-
NodeId = new NodeId(path, NamespaceIndex),
41844267
BrowseName = new QualifiedName(path, NamespaceIndex),
41854268
DisplayName = new LocalizedText("en", name),
41864269
WriteMask = AttributeWriteMask.None,
@@ -4240,7 +4323,6 @@ private MultiStateValueDiscreteState CreateMultiStateValueDiscreteItemVariable(
42404323
{
42414324
var variable = new MultiStateValueDiscreteState(parent)
42424325
{
4243-
NodeId = new NodeId(path, NamespaceIndex),
42444326
BrowseName = new QualifiedName(path, NamespaceIndex),
42454327
DisplayName = new LocalizedText("en", name),
42464328
WriteMask = AttributeWriteMask.None,

0 commit comments

Comments
 (0)