Apply code style formatting and restore partial modifiers on Avalonia views

Linter/formatter pass across the full codebase. Restores required partial
keyword on AXAML code-behind classes that the formatter incorrectly removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-31 07:58:13 -04:00
parent 55ef854612
commit 41a6b66943
221 changed files with 4274 additions and 3823 deletions

View File

@@ -16,23 +16,54 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
Hierarchy = new List<GalaxyObjectInfo>
{
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
new()
{
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
}
},
Attributes = new List<GalaxyAttributeInfo>
{
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "FreeAttr", FullTagReference = "TestObj.FreeAttr", MxDataType = 5, SecurityClassification = 0 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "OperateAttr", FullTagReference = "TestObj.OperateAttr", MxDataType = 5, SecurityClassification = 1 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "SecuredAttr", FullTagReference = "TestObj.SecuredAttr", MxDataType = 5, SecurityClassification = 2 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "VerifiedAttr", FullTagReference = "TestObj.VerifiedAttr", MxDataType = 5, SecurityClassification = 3 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "TuneAttr", FullTagReference = "TestObj.TuneAttr", MxDataType = 5, SecurityClassification = 4 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "ConfigAttr", FullTagReference = "TestObj.ConfigAttr", MxDataType = 5, SecurityClassification = 5 },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "ViewOnlyAttr", FullTagReference = "TestObj.ViewOnlyAttr", MxDataType = 5, SecurityClassification = 6 },
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "FreeAttr",
FullTagReference = "TestObj.FreeAttr", MxDataType = 5, SecurityClassification = 0
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "OperateAttr",
FullTagReference = "TestObj.OperateAttr", MxDataType = 5, SecurityClassification = 1
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "SecuredAttr",
FullTagReference = "TestObj.SecuredAttr", MxDataType = 5, SecurityClassification = 2
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "VerifiedAttr",
FullTagReference = "TestObj.VerifiedAttr", MxDataType = 5, SecurityClassification = 3
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "TuneAttr",
FullTagReference = "TestObj.TuneAttr", MxDataType = 5, SecurityClassification = 4
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "ConfigAttr",
FullTagReference = "TestObj.ConfigAttr", MxDataType = 5, SecurityClassification = 5
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "ViewOnlyAttr",
FullTagReference = "TestObj.ViewOnlyAttr", MxDataType = 5, SecurityClassification = 6
}
}
};
}
/// <summary>
/// Verifies that writable Galaxy security classifications publish OPC UA variables with read-write access.
/// Verifies that writable Galaxy security classifications publish OPC UA variables with read-write access.
/// </summary>
[Fact]
public async Task ReadWriteAttribute_HasCurrentReadOrWrite_AccessLevel()
@@ -52,11 +83,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
$"{attrName} should be ReadWrite");
}
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that secured and view-only Galaxy classifications publish OPC UA variables with read-only access.
/// Verifies that secured and view-only Galaxy classifications publish OPC UA variables with read-only access.
/// </summary>
[Fact]
public async Task ReadOnlyAttribute_HasCurrentRead_AccessLevel()
@@ -76,11 +110,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
$"{attrName} should be ReadOnly");
}
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that the bridge rejects writes against Galaxy attributes whose security classification is read-only.
/// Verifies that the bridge rejects writes against Galaxy attributes whose security classification is read-only.
/// </summary>
[Fact]
public async Task Write_ToReadOnlyAttribute_IsRejected()
@@ -96,17 +133,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var result = client.Write(nodeId, "test");
StatusCode.IsBad(result).ShouldBeTrue("Write to ReadOnly attribute should be rejected");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that writes succeed for Galaxy attributes whose security classification permits operator updates.
/// Verifies that writes succeed for Galaxy attributes whose security classification permits operator updates.
/// </summary>
[Fact]
public async Task Write_ToReadWriteAttribute_Succeeds()
{
var mxClient = new FakeMxAccessClient();
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient, repo: CreateRepoWithSecurityLevels());
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient, CreateRepoWithSecurityLevels());
await fixture.InitializeAsync();
try
{
@@ -117,7 +157,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var result = client.Write(nodeId, "test");
StatusCode.IsGood(result).ShouldBeTrue("Write to ReadWrite attribute should succeed");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
}
}
}

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
@@ -11,13 +9,13 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
/// <summary>
/// Integration tests verifying dynamic address space changes via a real OPC UA client.
/// Tests browse, subscribe, add/remove nodes at runtime, and subscription quality changes.
/// Integration tests verifying dynamic address space changes via a real OPC UA client.
/// Tests browse, subscribe, add/remove nodes at runtime, and subscription quality changes.
/// </summary>
public class AddressSpaceRebuildTests
{
/// <summary>
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
/// Confirms that the initial browsed hierarchy matches the seeded Galaxy model.
/// </summary>
[Fact]
public async Task Browse_ReturnsInitialHierarchy()
@@ -42,7 +40,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
/// Confirms that adding a Galaxy object and rebuilding exposes the new node to OPC UA clients.
/// </summary>
[Fact]
public async Task Browse_AfterAddingObject_NewNodeAppears()
@@ -88,7 +86,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
/// Confirms that removing a Galaxy object and rebuilding removes the node from the OPC UA hierarchy.
/// </summary>
[Fact]
public async Task Browse_AfterRemovingObject_NodeDisappears()
@@ -124,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
/// Confirms that subscriptions on deleted nodes receive a bad-quality notification after rebuild.
/// </summary>
[Fact]
public async Task Subscribe_RemovedNode_PublishesBadQuality()
@@ -138,7 +136,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// Subscribe to an attribute that will be removed
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInBatchID");
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
// Collect notifications
var notifications = new List<MonitoredItemNotification>();
@@ -173,7 +171,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
/// Confirms that subscriptions on surviving nodes continue to work after a partial rebuild.
/// </summary>
[Fact]
public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild()
@@ -187,7 +185,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// Subscribe to an attribute that will survive the rebuild
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
await Task.Delay(500);
@@ -212,7 +210,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
/// Confirms that adding a Galaxy attribute and rebuilding exposes a new OPC UA variable.
/// </summary>
[Fact]
public async Task Browse_AddAttribute_NewVariableAppears()
@@ -249,7 +247,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
/// Confirms that removing a Galaxy attribute and rebuilding removes the OPC UA variable.
/// </summary>
[Fact]
public async Task Browse_RemoveAttribute_VariableDisappears()
@@ -266,8 +264,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
initialChildren.ShouldContain(c => c.Name == "MachineCode");
// Remove MachineCode attribute
fixture.GalaxyRepository!.Attributes.RemoveAll(
a => a.TagName == "TestMachine_001" && a.AttributeName == "MachineCode");
fixture.GalaxyRepository!.Attributes.RemoveAll(a =>
a.TagName == "TestMachine_001" && a.AttributeName == "MachineCode");
fixture.Service.TriggerRebuild();
await Task.Delay(500);
@@ -282,7 +280,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
/// Confirms that rebuilds preserve subscription bookkeeping for nodes that survive the metadata refresh.
/// </summary>
[Fact]
public async Task Rebuild_PreservesSubscriptionBookkeeping_ForSurvivingNodes()
@@ -312,7 +310,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that transferred monitored items recreate MXAccess subscriptions when the service has no local subscription state.
/// Confirms that transferred monitored items recreate MXAccess subscriptions when the service has no local
/// subscription state.
/// </summary>
[Fact]
public async Task TransferSubscriptions_RestoresMxAccessSubscriptionState_WhenLocalStateIsMissing()
@@ -347,7 +346,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that transferring monitored items does not double-count subscriptions already tracked in memory.
/// Confirms that transferring monitored items does not double-count subscriptions already tracked in memory.
/// </summary>
[Fact]
public async Task TransferSubscriptions_DoesNotDoubleCount_WhenSubscriptionAlreadyTracked()
@@ -380,4 +379,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
}
}
}

View File

@@ -2,7 +2,6 @@ using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
@@ -11,12 +10,12 @@ using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
/// <summary>
/// Verifies OPC UA indexed array writes against the bridge's whole-array runtime update behavior.
/// Verifies OPC UA indexed array writes against the bridge's whole-array runtime update behavior.
/// </summary>
public class ArrayWriteTests
{
/// <summary>
/// Confirms that writing a single array element updates the correct slot while preserving the rest of the array.
/// Confirms that writing a single array element updates the correct slot while preserving the rest of the array.
/// </summary>
[Fact]
public async Task Write_SingleArrayElement_UpdatesWholeArrayValue()
@@ -39,7 +38,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
before.Length.ShouldBe(50);
before[1].ShouldBe("PART-01");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
StatusCode.IsGood(status).ShouldBe(true);
var after = client.Read(nodeId).Value as string[];
@@ -56,7 +55,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that array nodes use bracketless OPC UA node identifiers while still exposing one-dimensional array metadata.
/// Confirms that array nodes use bracketless OPC UA node identifiers while still exposing one-dimensional array
/// metadata.
/// </summary>
[Fact]
public async Task ArrayNode_UsesBracketlessNodeId_AndPublishesArrayDimensions()
@@ -91,7 +91,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that a null runtime value for a statically sized array is exposed as a typed fixed-length array.
/// Confirms that a null runtime value for a statically sized array is exposed as a typed fixed-length array.
/// </summary>
[Fact]
public async Task Read_NullStaticArray_ReturnsDefaultTypedArray()
@@ -122,7 +122,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that an indexed write also updates the published OPC UA value seen by subscribed clients.
/// Confirms that an indexed write also updates the published OPC UA value seen by subscribed clients.
/// </summary>
[Fact]
public async Task Write_SingleArrayElement_PublishesUpdatedArrayToSubscribers()
@@ -139,7 +139,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers");
var notifications = new ConcurrentBag<MonitoredItemNotification>();
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
item.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification notification)
@@ -148,7 +148,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await Task.Delay(500);
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
StatusCode.IsGood(status).ShouldBe(true);
await Task.Delay(1000);
@@ -169,7 +169,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that indexed writes succeed even when the current runtime array value is null.
/// Confirms that indexed writes succeed even when the current runtime array value is null.
/// </summary>
[Fact]
public async Task Write_SingleArrayElement_WhenCurrentArrayIsNull_UsesDefaultArray()
@@ -184,7 +184,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await client.ConnectAsync(fixture.EndpointUrl);
var nodeId = client.MakeNodeId("MESReceiver_001.MoveInPartNumbers");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, indexRange: "1");
var status = client.Write(nodeId, new[] { "UPDATED-PART" }, "1");
StatusCode.IsGood(status).ShouldBe(true);
var after = client.Read(nodeId).Value as string[];
@@ -200,4 +200,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
}
}
}

View File

@@ -16,19 +16,34 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
Hierarchy = new List<GalaxyObjectInfo>
{
new GalaxyObjectInfo { GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false }
new()
{
GobjectId = 1, TagName = "TestObj", BrowseName = "TestObj", ParentGobjectId = 0, IsArea = false
}
},
Attributes = new List<GalaxyAttributeInfo>
{
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "HistorizedAttr", FullTagReference = "TestObj.HistorizedAttr", MxDataType = 2, IsHistorized = true },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "NormalAttr", FullTagReference = "TestObj.NormalAttr", MxDataType = 5, IsHistorized = false },
new GalaxyAttributeInfo { GobjectId = 1, TagName = "TestObj", AttributeName = "AlarmAttr", FullTagReference = "TestObj.AlarmAttr", MxDataType = 1, IsAlarm = true },
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "HistorizedAttr",
FullTagReference = "TestObj.HistorizedAttr", MxDataType = 2, IsHistorized = true
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "NormalAttr",
FullTagReference = "TestObj.NormalAttr", MxDataType = 5, IsHistorized = false
},
new()
{
GobjectId = 1, TagName = "TestObj", AttributeName = "AlarmAttr",
FullTagReference = "TestObj.AlarmAttr", MxDataType = 1, IsAlarm = true
}
}
};
}
/// <summary>
/// Verifies that historized Galaxy attributes advertise OPC UA historizing support and history-read access.
/// Verifies that historized Galaxy attributes advertise OPC UA historizing support and history-read access.
/// </summary>
[Fact]
public async Task HistorizedAttribute_HasHistorizingTrue_AndHistoryReadAccess()
@@ -49,11 +64,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
(level & AccessLevels.HistoryRead).ShouldBe(AccessLevels.HistoryRead,
"HistoryRead bit should be set");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that non-historized Galaxy attributes do not claim OPC UA history support.
/// Verifies that non-historized Galaxy attributes do not claim OPC UA history support.
/// </summary>
[Fact]
public async Task NormalAttribute_HasHistorizingFalse_AndNoHistoryReadAccess()
@@ -71,10 +89,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var accessLevel = client.ReadAttribute(nodeId, Attributes.AccessLevel);
var level = (byte)accessLevel.Value;
(level & AccessLevels.HistoryRead).ShouldBe((byte)0,
(level & AccessLevels.HistoryRead).ShouldBe(0,
"HistoryRead bit should not be set");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
}
}
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Opc.Ua;
@@ -12,7 +11,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
public class IncrementalSyncTests
{
/// <summary>
/// Verifies that adding a new Galaxy object and attribute causes the corresponding OPC UA node subtree to appear after sync.
/// Verifies that adding a new Galaxy object and attribute causes the corresponding OPC UA node subtree to appear after
/// sync.
/// </summary>
[Fact]
public async Task Sync_AddObject_NewNodeAppears()
@@ -56,11 +56,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// Original object should still be there
children.Select(c => c.Name).ShouldContain("TestMachine_001");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that removing a Galaxy object tears down the corresponding OPC UA subtree without affecting siblings.
/// Verifies that removing a Galaxy object tears down the corresponding OPC UA subtree without affecting siblings.
/// </summary>
[Fact]
public async Task Sync_RemoveObject_NodeDisappears()
@@ -90,11 +93,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// DelmiaReceiver should still be there
children.Select(c => c.Name).ShouldContain("DelmiaReceiver");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that adding a Galaxy attribute creates a new OPC UA variable during incremental rebuild.
/// Verifies that adding a Galaxy attribute creates a new OPC UA variable during incremental rebuild.
/// </summary>
[Fact]
public async Task Sync_AddAttribute_NewVariableAppears()
@@ -120,17 +126,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
children.Select(c => c.Name).ShouldContain("NewAttr");
children.Select(c => c.Name).ShouldContain("MachineID");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that subscriptions on unchanged objects continue receiving data after unrelated subtree rebuilds.
/// Verifies that subscriptions on unchanged objects continue receiving data after unrelated subtree rebuilds.
/// </summary>
[Fact]
public async Task Sync_UnchangedObject_SubscriptionSurvives()
{
var mxClient = new FakeMxAccessClient();
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient);
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient);
await fixture.InitializeAsync();
try
{
@@ -139,7 +148,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// Subscribe to MachineID on TestMachine_001
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
var (sub, item) = await client.SubscribeAsync(nodeId, 250);
var (sub, item) = await client.SubscribeAsync(nodeId);
await Task.Delay(500);
// Modify a DIFFERENT object (MESReceiver) — TestMachine_001 should be unaffected
@@ -157,11 +166,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var lastValue = (item.LastValue as MonitoredItemNotification)?.Value?.Value;
lastValue.ShouldBe("UPDATED");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
/// <summary>
/// Verifies that a rebuild request with no repository changes leaves the published namespace intact.
/// Verifies that a rebuild request with no repository changes leaves the published namespace intact.
/// </summary>
[Fact]
public async Task Sync_NoChanges_NothingHappens()
@@ -181,7 +193,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001"));
children.Select(c => c.Name).ShouldContain("MachineID");
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
}
}
}

View File

@@ -7,20 +7,19 @@ using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
{
/// <summary>
/// Integration tests verifying multi-client subscription sync and concurrent operations.
/// Integration tests verifying multi-client subscription sync and concurrent operations.
/// </summary>
public class MultiClientTests
{
// ── Subscription Sync ─────────────────────────────────────────────
/// <summary>
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
/// Confirms that multiple OPC UA clients subscribed to the same tag all receive the same runtime update.
/// </summary>
[Fact]
public async Task MultipleClients_SubscribeToSameTag_AllReceiveDataChanges()
@@ -33,14 +32,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var notifications = new ConcurrentDictionary<int, List<MonitoredItemNotification>>();
var subscriptions = new List<Subscription>();
for (int i = 0; i < 3; i++)
for (var i = 0; i < 3; i++)
{
var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
clients.Add(client);
var nodeId = client.MakeNodeId("TestMachine_001.MachineID");
var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100);
var (sub, item) = await client.SubscribeAsync(nodeId, 100);
subscriptions.Add(sub);
var clientIndex = i;
@@ -55,14 +54,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await Task.Delay(500); // let subscriptions settle
// Simulate data change
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "MACHINE_42", 192);
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "MACHINE_42");
await Task.Delay(1000); // let publish cycle deliver
// All 3 clients should have received the notification
for (int i = 0; i < 3; i++)
{
for (var i = 0; i < 3; i++)
notifications[i].Count.ShouldBeGreaterThan(0, $"Client {i} did not receive notification");
}
foreach (var sub in subscriptions) await sub.DeleteAsync(true);
foreach (var c in clients) c.Dispose();
@@ -74,7 +71,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
/// Confirms that one client disconnecting does not stop remaining clients from receiving updates.
/// </summary>
[Fact]
public async Task Client_Disconnects_OtherClientsStillReceive()
@@ -97,8 +94,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var (sub2, _) = await client2.SubscribeAsync(client2.MakeNodeId("TestMachine_001.MachineID"), 100);
var (sub3, item3) = await client3.SubscribeAsync(client3.MakeNodeId("TestMachine_001.MachineID"), 100);
item1.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n); };
item3.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications3.Add(n); };
item1.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n);
};
item3.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n) notifications3.Add(n);
};
await Task.Delay(500);
@@ -108,11 +111,13 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await Task.Delay(500); // let server process disconnect
// Simulate data change — should not crash, clients 1+3 should still receive
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_DISCONNECT", 192);
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_DISCONNECT");
await Task.Delay(1000);
notifications1.Count.ShouldBeGreaterThan(0, "Client 1 should still receive after client 2 disconnected");
notifications3.Count.ShouldBeGreaterThan(0, "Client 3 should still receive after client 2 disconnected");
notifications1.Count.ShouldBeGreaterThan(0,
"Client 1 should still receive after client 2 disconnected");
notifications3.Count.ShouldBeGreaterThan(0,
"Client 3 should still receive after client 2 disconnected");
await sub1.DeleteAsync(true);
await sub3.DeleteAsync(true);
@@ -126,7 +131,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
/// Confirms that one client unsubscribing does not interrupt delivery to other subscribed clients.
/// </summary>
[Fact]
public async Task Client_Unsubscribes_OtherClientsStillReceive()
@@ -144,7 +149,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var (sub1, _) = await client1.SubscribeAsync(client1.MakeNodeId("TestMachine_001.MachineID"), 100);
var (sub2, item2) = await client2.SubscribeAsync(client2.MakeNodeId("TestMachine_001.MachineID"), 100);
item2.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n); };
item2.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n);
};
await Task.Delay(500);
@@ -153,10 +161,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await Task.Delay(500);
// Simulate data change — client 2 should still receive
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_UNSUB", 192);
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "AFTER_UNSUB");
await Task.Delay(1000);
notifications2.Count.ShouldBeGreaterThan(0, "Client 2 should still receive after client 1 unsubscribed");
notifications2.Count.ShouldBeGreaterThan(0,
"Client 2 should still receive after client 1 unsubscribed");
await sub2.DeleteAsync(true);
client1.Dispose();
@@ -169,7 +178,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
/// Confirms that clients subscribed to different tags only receive updates for their own monitored data.
/// </summary>
[Fact]
public async Task MultipleClients_SubscribeToDifferentTags_EachGetsOwnData()
@@ -187,15 +196,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var notifications2 = new ConcurrentBag<MonitoredItemNotification>();
var (sub1, item1) = await client1.SubscribeAsync(client1.MakeNodeId("TestMachine_001.MachineID"), 100);
var (sub2, item2) = await client2.SubscribeAsync(client2.MakeNodeId("DelmiaReceiver_001.DownloadPath"), 100);
var (sub2, item2) =
await client2.SubscribeAsync(client2.MakeNodeId("DelmiaReceiver_001.DownloadPath"), 100);
item1.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n); };
item2.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n); };
item1.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n) notifications1.Add(n);
};
item2.Notification += (_, e) =>
{
if (e.NotificationValue is MonitoredItemNotification n) notifications2.Add(n);
};
await Task.Delay(500);
// Only change MachineID
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "CHANGED", 192);
fixture.MxProxy!.SimulateDataChangeByAddress("TestMachine_001.MachineID", "CHANGED");
await Task.Delay(1000);
notifications1.Count.ShouldBeGreaterThan(0, "Client 1 should receive MachineID change");
@@ -219,7 +235,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// ── Concurrent Operation Tests ────────────────────────────────────
/// <summary>
/// Confirms that concurrent browse operations from several clients all complete successfully.
/// Confirms that concurrent browse operations from several clients all complete successfully.
/// </summary>
[Fact]
public async Task ConcurrentBrowseFromMultipleClients_AllSucceed()
@@ -230,7 +246,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
try
{
var clients = new List<OpcUaTestClient>();
for (int i = 0; i < 5; i++)
for (var i = 0; i < 5; i++)
{
var c = new OpcUaTestClient();
await c.ConnectAsync(fixture.EndpointUrl);
@@ -262,7 +278,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that concurrent browse requests return consistent results across clients.
/// Confirms that concurrent browse requests return consistent results across clients.
/// </summary>
[Fact]
public async Task ConcurrentBrowse_AllReturnSameResults()
@@ -272,7 +288,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
try
{
var clients = new List<OpcUaTestClient>();
for (int i = 0; i < 5; i++)
for (var i = 0; i < 5; i++)
{
var c = new OpcUaTestClient();
await c.ConnectAsync(fixture.EndpointUrl);
@@ -287,7 +303,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
// All should get identical child lists
var firstResult = results[0].Select(r => r.Name).OrderBy(n => n).ToList();
for (int i = 1; i < results.Length; i++)
for (var i = 1; i < results.Length; i++)
{
var thisResult = results[i].Select(r => r.Name).OrderBy(n => n).ToList();
thisResult.ShouldBe(firstResult, $"Client {i} got different browse results");
@@ -302,7 +318,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
/// Confirms that simultaneous browse and subscribe operations do not interfere with one another.
/// </summary>
[Fact]
public async Task ConcurrentBrowseAndSubscribe_NoInterference()
@@ -312,7 +328,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
try
{
var clients = new List<OpcUaTestClient>();
for (int i = 0; i < 4; i++)
for (var i = 0; i < 4; i++)
{
var c = new OpcUaTestClient();
await c.ConnectAsync(fixture.EndpointUrl);
@@ -340,7 +356,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
/// Confirms that concurrent subscribe, read, and browse operations complete without deadlocking the server.
/// </summary>
[Fact]
public async Task ConcurrentSubscribeAndRead_NoDeadlock()
@@ -380,7 +396,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
/// <summary>
/// Confirms that repeated client churn does not leave the server in an unstable state.
/// Confirms that repeated client churn does not leave the server in an unstable state.
/// </summary>
[Fact]
public async Task RapidConnectDisconnect_ServerStaysStable()
@@ -390,7 +406,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
try
{
// Rapidly connect, browse, disconnect — 10 iterations
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
using var client = new OpcUaTestClient();
await client.ConnectAsync(fixture.EndpointUrl);
@@ -411,4 +427,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
}
}
}

View File

@@ -18,7 +18,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
.AddUser("writetune", "writetune123", AppRoles.WriteTune)
.AddUser("writeconfig", "writeconfig123", AppRoles.WriteConfigure)
.AddUser("alarmack", "alarmack123", AppRoles.AlarmAck)
.AddUser("admin", "admin123", AppRoles.ReadOnly, AppRoles.WriteOperate, AppRoles.WriteTune, AppRoles.WriteConfigure, AppRoles.AlarmAck);
.AddUser("admin", "admin123", AppRoles.ReadOnly, AppRoles.WriteOperate, AppRoles.WriteTune,
AppRoles.WriteConfigure, AppRoles.AlarmAck);
}
private static AuthenticationConfiguration CreateAuthConfig(bool anonymousCanWrite = false)
@@ -36,7 +37,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var mxClient = new FakeMxAccessClient();
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("hello");
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
mxClient: mxClient,
mxClient,
authConfig: CreateAuthConfig(),
authProvider: CreateTestAuthProvider());
await fixture.InitializeAsync();
@@ -48,14 +49,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var result = client.Read(client.MakeNodeId("TestMachine_001.MachineID"));
result.StatusCode.ShouldNotBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
public async Task AnonymousWrite_Denied_WhenAnonymousCanWriteFalse()
{
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
authConfig: CreateAuthConfig(anonymousCanWrite: false),
authConfig: CreateAuthConfig(false),
authProvider: CreateTestAuthProvider());
await fixture.InitializeAsync();
try
@@ -66,7 +70,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -75,8 +82,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var mxClient = new FakeMxAccessClient();
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
mxClient: mxClient,
authConfig: CreateAuthConfig(anonymousCanWrite: true),
mxClient,
authConfig: CreateAuthConfig(true),
authProvider: CreateTestAuthProvider());
await fixture.InitializeAsync();
try
@@ -87,7 +94,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -105,7 +115,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -114,7 +127,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var mxClient = new FakeMxAccessClient();
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
mxClient: mxClient,
mxClient,
authConfig: CreateAuthConfig(),
authProvider: CreateTestAuthProvider());
await fixture.InitializeAsync();
@@ -126,7 +139,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -144,7 +160,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -153,7 +172,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var mxClient = new FakeMxAccessClient();
mxClient.TagValues["TestMachine_001.MachineID"] = Vtq.Good("initial");
var fixture = OpcUaServerFixture.WithFakeMxAccessClient(
mxClient: mxClient,
mxClient,
authConfig: CreateAuthConfig(),
authProvider: CreateTestAuthProvider());
await fixture.InitializeAsync();
@@ -165,7 +184,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var status = client.Write(client.MakeNodeId("TestMachine_001.MachineID"), "test");
status.Code.ShouldNotBe(StatusCodes.BadUserAccessDenied);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -182,7 +204,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
await Should.ThrowAsync<ServiceResultException>(async () =>
await client.ConnectAsync(fixture.EndpointUrl, username: "readonly", password: "wrongpassword"));
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
}
}
}

View File

@@ -26,7 +26,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var serviceLevel = client.Read(VariableIds.Server_ServiceLevel);
((byte)serviceLevel.Value).ShouldBe((byte)255);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -53,7 +56,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
var redundancySupport = client.Read(VariableIds.Server_ServerRedundancy_RedundancySupport);
((int)redundancySupport.Value).ShouldBe((int)RedundancySupport.Warm);
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -130,7 +136,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
uris.ShouldContain("urn:test:server2");
}
}
finally { await fixture.DisposeAsync(); }
finally
{
await fixture.DisposeAsync();
}
}
[Fact]
@@ -176,4 +185,4 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration
}
}
}
}
}